AniSkip (aniskip v0.1.0)

Copy Markdown View Source

A client for AniSkip, the community database of where an anime episode's opening and ending are. 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, not episode 13 of the show).

{: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…"}]

Each time says how long the file it was measured on was (length), because a different cut of the same episode (a BD release with a preview, a TV rip without the recap) has the opening somewhere else. fit/2 keeps the times that match the file you are playing and gives you one :intro and one :credits to offer, and at/3 says which to show at the current position.

skips = AniSkip.fit(times, video_duration)
AniSkip.at(skips, current_time)   #=> %{kind: :intro, from: 58.6, to: 148.6, tail: false} | nil

Nothing here is cached: AniSkip asks to be used gently, so keep each episode's answer for a day in your own cache (an episode it does not know comes back as {:ok, []}, and is worth remembering too). Every request merges :req_options (per call or config :aniskip, :req_options), so the client can be pointed at Req.Test.

Summary

Types

An opening (:op) or ending (:ed); AniSkip's mixed-op/mixed-ed are read as the same.

A skip to offer: tail is true when it runs to the end of the file (credits).

One time AniSkip knows, in seconds: length is the episode length it was measured on (nil when not recorded), id its skip id (for vote/3).

Functions

The skip to offer at now seconds into the file: the first one now is inside of, at least :settle seconds in (default 1.5, so a button does not flash at a boundary) and more than two seconds before its end. nil when there is none.

The skips to offer for a file total seconds long: the first opening as :intro and the first ending as :credits, in the order given. A time measured on a file of another length (more than tolerance seconds apart, default 5) belongs to another cut and is dropped — unless total is still short of that length, as it is while a conversion is under way and the file is growing, when it is kept. With no total (nil or 0) everything fits.

Reads AniSkip's answer (%{"results" => [...]}) into times. A result whose end is not after its start, or of a type not asked about (a recap), is left out.

Submit a time for episode episode of MAL entry mal_id: kind is :op or :ed, from/to in seconds, length the file's length in seconds (what later readers match against), and submitter an id for the person submitting — AniSkip wants a UUID, kept stable per user so their submissions can be rate-limited and weighed together. :provider names the source (default "custom"). {:ok, skip_id} on success.

The times AniSkip knows for episode episode of MAL entry mal_id: {:ok, [time]}, {:ok, []} when it knows none (its 404), {:error, _} when it could not be asked.

Vote a time up or down by its id (from times/3). AniSkip hides times that are voted down enough. :ok on success.

Types

kind()

@type kind() :: :op | :ed

An opening (:op) or ending (:ed); AniSkip's mixed-op/mixed-ed are read as the same.

skip()

@type skip() :: %{
  kind: :intro | :credits,
  from: float(),
  to: float(),
  tail: boolean()
}

A skip to offer: tail is true when it runs to the end of the file (credits).

time()

@type time() :: %{
  kind: kind(),
  from: float(),
  to: float(),
  length: float() | nil,
  id: String.t() | nil
}

One time AniSkip knows, in seconds: length is the episode length it was measured on (nil when not recorded), id its skip id (for vote/3).

Functions

at(skips, now, opts \\ [])

@spec at([skip()], number(), keyword()) :: skip() | nil

The skip to offer at now seconds into the file: the first one now is inside of, at least :settle seconds in (default 1.5, so a button does not flash at a boundary) and more than two seconds before its end. nil when there is none.

fit(times, total, opts \\ [])

@spec fit([time()], number() | nil, keyword()) :: [skip()]

The skips to offer for a file total seconds long: the first opening as :intro and the first ending as :credits, in the order given. A time measured on a file of another length (more than tolerance seconds apart, default 5) belongs to another cut and is dropped — unless total is still short of that length, as it is while a conversion is under way and the file is growing, when it is kept. With no total (nil or 0) everything fits.

tail marks a skip that runs to within three seconds of the end: credits with nothing after them, which a player can offer as "next episode".

parse(arg1)

@spec parse(term()) :: [time()]

Reads AniSkip's answer (%{"results" => [...]}) into times. A result whose end is not after its start, or of a type not asked about (a recap), is left out.

submit(mal_id, episode, kind, from, to, length, submitter, opts \\ [])

@spec submit(
  pos_integer(),
  pos_integer(),
  kind(),
  number(),
  number(),
  number(),
  String.t(),
  keyword()
) ::
  {:ok, String.t()} | {:error, term()}

Submit a time for episode episode of MAL entry mal_id: kind is :op or :ed, from/to in seconds, length the file's length in seconds (what later readers match against), and submitter an id for the person submitting — AniSkip wants a UUID, kept stable per user so their submissions can be rate-limited and weighed together. :provider names the source (default "custom"). {:ok, skip_id} on success.

times(mal_id, episode, opts \\ [])

@spec times(pos_integer(), pos_integer(), keyword()) ::
  {:ok, [time()]} | {:error, term()}

The times AniSkip knows for episode episode of MAL entry mal_id: {:ok, [time]}, {:ok, []} when it knows none (its 404), {:error, _} when it could not be asked.

Options:

  • :types — which to ask for, from :op, :ed, :mixed_op, :mixed_ed (default all four).
  • :episode_length — your file's length in seconds; AniSkip then returns only times measured on a file of about that length. The default 0 asks for every time, which with fit/2 is the better way: you see what is known before deciding.
  • :req_options — merged into the request (Req.Test, timeouts).

vote(id, direction, opts \\ [])

@spec vote(String.t(), :up | :down, keyword()) :: :ok | {:error, term()}

Vote a time up or down by its id (from times/3). AniSkip hides times that are voted down enough. :ok on success.