# `Laev.Position`
[🔗](https://github.com/alexdont/laev/blob/main/lib/laev/position.ex#L1)

Exact playback-position memory, crash-safe.

A tiny Lua script (mpv has a built-in Lua engine — no dependencies) writes
the current second to a file every 5 seconds while mpv plays, so the
position survives player crashes and power loss. The file is keyed by
title + episode — NOT by stream URL — so switching to a different source
of the same episode resumes from the same spot.

Positions under 30s aren't kept (no point), and finishing a title (>95%)
clears it so a rewatch starts from the beginning.

# `adopt`

Carry a saved position from one key to another, once.

Anime used to be filed under its TMDB show and is now filed under its
MyAnimeList entry; an episode somebody was seven minutes into should still be
seven minutes in. Copies only when there is something to copy and nothing
already there, so it is safe to call on every play.

# `anime_finished`

Every anime marked finished, as a list of MAL ids.

# `anime_marks`

Watched anime episodes, as `%{mal_id => count}`.

Kept apart from the show marks above rather than mixed in with them: anime is
counted by MyAnimeList entry and finished according to MyAnimeList, so a shelf
that counts TMDB shows must not find 300 anime in the same bag and call them
all unfinished series.

# `device_id`

A short id for this machine, made once and kept. Deliberately not synced —
every device needs its own, or they would all write to the same log.

# `episode_marks`

Every watched episode mark, grouped as `%{{type, id} => %{season => count}}`.

One directory read for a whole screen: a list of twenty rows each asking after
its own show would otherwise scan a thousand files twenty times. An episode
numbered absolutely (anime) lands under season 0, where it is still countable
without pretending to know which season it belongs to.

# `finished?`

True when this title/episode was watched to the end (mpv hit eof).

# `forget`

Erase every saved position and play record for a title — the title's own
file and each of its episodes. Returns how many files went.

This is the destructive half of removing something from your history: those
files are the watched marks and the only record of time spent, so a show
forgotten here leaves the stats too.

# `heartbeat_at`

When the play counter was last written (posix seconds), or nil.

The mpv script rewrites it every 5 seconds while the player is up, so this
is a heartbeat: recent means mpv is still there. Unlike the position file
it is never touched by a sync pull, so a sync can't fake one.

# `last_saved_at`

When the position file was last written (posix seconds), or nil. The Lua
tracker saves every 5s while mpv runs — a stale mtime means mpv is gone.

# `log_file`

Where this machine's day-by-day watch log lives.

One file per device, because the log is append-only and the calendar is the
sum of all of them. A single shared file could only ever be merged by letting
one device's history overwrite another's; a file per device merges exactly,
since nothing but this machine ever writes to this one.

# `mark_state`

What laev knows about this title or episode: `:none`, `:watched` (played
through or marked by hand), or `{:partial, seconds}` — somewhere in the middle.

Asked before writing a mark on someone's behalf, so an import can tell the
difference between a gap it should fill and a position it must not stamp over.

# `mpv_args`

mpv arguments for position tracking + resume, as `{args, resume_at}`
where `resume_at` is a human-readable time when resuming, else nil.
Best-effort: any failure returns `{[], nil}` — never breaks playback.

# `played`

How much of this title was actually watched, and how much was skipped past,
in seconds — `nil` for anything played before laev started measuring. Only
the mpv script writes it, so it covers real playback and nothing else.

# `promote_finished`

Promote a numeric position to "done" — played through, by the 85% rule.

`watched?/2` has always treated a saved position past 85% of the runtime as
watched, which retroactively covers episodes finished before the mpv script
wrote "done" at the end. But that answer lived only at read time, wherever a
runtime happened to be in hand: the season picker showed ✓ while the counters
that scan the directory saw a number — "everything is watched" on one screen,
"7 of 10" on the next. Writing the word down makes every screen read the same
file the same way.

Only ever promotes a number; "done", "seen" and short positions stay put.

# `promote_watched`

The same, swept across the whole directory with whatever runtimes are already
known — `runtime_of` maps a position file name to that title's typical episode
(or film) length in seconds, or nil where nothing knows. Returns how many were
promoted. Conservative by construction: a title with no cached runtime is left
for the episode list to reconcile with the real one.

# `record`

Record a sample of playback: where the playhead is, how much has been watched
and skipped, and whether that counts as finished.

What the mpv Lua script does from inside the player, available from outside
it, so a player laev cannot put a script into can still be followed. The
rules are the script's own: a position under `floor` is the beginning rather
than a place to come back to, finishing writes the watched marker instead of
a number, and the day's log grows by `log_seconds` — whatever the caller has
watched since it last said so.

# `resume_at`

Human-readable stored position for a play context, or nil.

# `saved_position`

The saved spot in seconds, or nil — the same number `--start=` is built from.

# `set_watched`

Mark a title or episode watched by hand, or clear the mark.

Writes "seen" rather than the "done" playback leaves behind: it says you
have watched this, not that laev watched you watch it, so the stats can
count it as watched without adding a runtime it never measured.

# `watched?`

True when this episode counts as watched: marked "done", OR — when the
episode runtime (seconds) is known — the saved position is past 85% of it.
The runtime check retroactively catches episodes watched before the 85%
marker existed (their position is a plain number, not "done").

---

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