An Elixir client for AniSkip, the community database of where an anime episode's opening and ending are — what "Skip intro" is made of.

def deps do
  [{:aniskip, "~> 0.1"}]
end

Where the opening is

Entries are keyed by MyAnimeList id and the episode's number within that MAL entry (a second season's first episode is episode 1 of the season's own entry).

{:ok, times} = AniSkip.times(5114, 1)
#=> [%{kind: :op, from: 58.6, to: 148.6, length: 1469.6, id: "067c…"},
#    %{kind: :ed, from: 1349.9, to: 1474.0, length: 1474.7, id: "473e…"}]

An episode AniSkip does not know is {:ok, []}.

Fitting them to your file

Each time says how long the file it was measured on was. A different cut of the same episode (a Blu-ray with a preview, a TV rip without the recap) has the opening somewhere else, so match the times to the file you are playing:

skips = AniSkip.fit(times, video_duration_seconds)
#=> [%{kind: :intro, from: 58.6, to: 148.6, tail: false},
#    %{kind: :credits, from: 1349.9, to: 1474.0, tail: true}]

AniSkip.at(skips, current_time)   #=> the skip to offer now, or nil

tail is credits that run to the end of the file — a player can offer those as "Next episode". A file still being converted (its length short of the measured one) keeps the times until it is long enough to judge.

Giving back

AniSkip.vote(skip_id, :up)
AniSkip.submit(mal_id, episode, :op, 58.6, 148.6, file_length, submitter_uuid)

Be gentle

Nothing here is cached. AniSkip is a small community service; keep each episode's answer for a day in your own cache ({:ok, []} too), and put a budget on lookups.

Errors and testing

Every function returns values and never raises: {:error, :rate_limited}, {:error, {:http, status}} or {:error, {:transport, reason}}. Every request merges req_options, so point it at Req.Test:

# config/test.exs
config :aniskip, req_options: [plug: {Req.Test, AniSkip}]

License

MIT © Alexander Don