# `AniSkip`
[🔗](https://github.com/alexdont/aniskip/blob/v0.1.0/lib/ani_skip.ex#L1)

A client for [AniSkip](https://api.aniskip.com/api-docs), 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`.

# `kind`

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

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

# `skip`

```elixir
@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`

```elixir
@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`).

# `at`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
