Skip to content

Getting started

By the end of this page, one machine runs the playlog-api server holding your watch history as an append-only event store. One mpv device records playback into it, and a second device resumes exactly where the first one stopped.

The pieces speak one HTTP API and nothing else, so they mix freely: a NixOS-managed server with standalone mpv clients, or the reverse, or all-standalone.

Two install paths run through the steps below, and you pick one per machine.

The nix path adds playlog as a flake input and uses the NixOS module services.playlog on the server plus the Home Manager module services.playlog-mpv on each mpv device.

The standalone path builds the two Rust binaries with cargo, drops the Lua plugin into any mpv setup, and runs the example systemd user units for the sync agent.

The server is a single binary with an SQLite database and no other moving parts.

Authentication is on by default: every /v1 request needs a bearer token, while /healthz stays open for liveness probes.

A fresh server therefore accepts no one until you issue the first token, which is the next step after it is running.

Add the flake input once; both modules and the packages come from it.

# in your flake's inputs
playlog.url = "github:Joaqim/playlog";

Then enable the service on the server machine, inside your NixOS configuration.

{
outputs = { self, nixpkgs, playlog }: {
nixosConfigurations.media = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
playlog.nixosModules.server
{
services.playlog = {
enable = true;
bind = "0.0.0.0:7600";
};
}
];
};
};
}

The default bind is 127.0.0.1:7600, which only the server machine itself can reach; setting it to your LAN or VPN address (0.0.0.0 binds all interfaces) is what lets devices connect.

The module manages no firewall or interfaces, so opening the port to the right network is yours to arrange.

Events land in /var/lib/playlog/events.db by default; the stateDir and dbPath options move it.

The service starts protected with auth on and no tokens issued, which is the intended starting posture.

Any Linux host with a Rust toolchain builds both binaries; only playlog-api is needed on the server.

Terminal window
git clone https://github.com/Joaqim/playlog
cd playlog
cargo build --release

Run the server on the address your devices can reach.

Terminal window
target/release/playlog-api --bind 0.0.0.0:7600

The full flag surface is small, and flags win over the environment:

  • --bind ADDR:PORT or PLAYLOG_BIND — listen address, default 127.0.0.1:7600.
  • --db PATH or PLAYLOG_DB — SQLite file, default ~/.local/state/playlog/events.db; the parent directory is created at startup.
  • --auth on|off or PLAYLOG_AUTH — bearer enforcement, default on.

With auth on, /v1 answers 401 to everyone until a token exists, so the first order of business after boot is issuing one.

The token is printed exactly once, prefixed plt_, and stored only as a hash — keep it somewhere safe when it appears.

Standalone, run the same binary with the same --db the server uses; with the defaults, no flag is needed.

Terminal window
target/release/playlog-api token add --label living-room

On NixOS, run it as root against the live database; the server can keep running while you do.

Terminal window
nix build github:Joaqim/playlog#playlog-api
sudo ./result/bin/playlog-api token add --db /var/lib/playlog/events.db --label living-room

A token’s label doubles as its device identity: events sent with it are stored under that device_id, so issue one token per device and name the labels accordingly.

token list shows labels and issue times, and token revoke --label NAME retires one.

If the server is reachable only over a network you already trust end to end — a VPN whose every peer is yours — you can drop tokens entirely: --auth off standalone, or services.playlog.auth.enable = false; on NixOS.

With auth off the network is the whole trust boundary, so keep it on for anything reachable beyond such a network.

Playlog’s mpv client is a Lua script; it appends events to a local spool file and never touches the network.

A sync agent ships the spool to the server and pulls the shared history into a local resume cache, which the script reads the next time a file loads.

The two paths below configure the same pieces.

Install the script into any mpv setup.

Terminal window
mkdir -p ~/.config/mpv/scripts ~/.config/mpv/script-opts
cp client/playlog.lua ~/.config/mpv/scripts/

Give the device a stable identity.

~/.config/mpv/script-opts/playlog.conf
device_id=living-room

That is the whole client configuration; the spool and resume cache default to ~/.local/state/playlog/, the same directory the sync agent reads and writes.

Put the credential and server address in a private environment file.

# ~/.config/playlog/token (chmod 600)
PLAYLOG_SYNC_TOKEN=plt_...
PLAYLOG_SYNC_URL=http://media.lan:7600

Install the agent and the example user units shipped in the repository.

Terminal window
sudo install target/release/playlog-sync /usr/local/bin/
cp sync/systemd/playlog-sync.* ~/.config/systemd/user/

In ~/.config/systemd/user/playlog-sync.service, uncomment the EnvironmentFile=%h/.config/playlog/token line, and adjust ExecStart= if you installed the binary somewhere other than /usr/local/bin.

Then start the units.

Terminal window
systemctl --user daemon-reload
systemctl --user enable --now playlog-sync.timer playlog-sync.path

The path unit ships the spool within seconds of each appended event, and the timer sweeps once a minute as the fallback.

Play a video and events appear in ~/.local/state/playlog/spool.jsonl, then leave for the server on the next sync run.

With the playlog input already added, bring the client module into a Home Manager configuration on the device.

homeConfigurations.living-room = home-manager.lib.homeManagerConfiguration {
pkgs = nixpkgs.legacyPackages.x86_64-linux;
modules = [
playlog.homeManagerModules.client
{
services.playlog-mpv = {
enable = true;
deviceId = "living-room";
sync.url = "http://media.lan:7600";
sync.tokenFile = "/home/you/.config/playlog/token";
};
}
];
};

The token file carries the same PLAYLOG_SYNC_TOKEN=plt_... line, mode 600, and is attached to the sync units for you.

There is deliberately no option taking the bare token string: a literal would land world-readably in the nix store, which is also why the path above is absolute rather than a flake-relative ./token.

The module installs the script, writes the script-opts from your deviceId, manages the spool, resume cache, and timer/path units, and enables programs.mpv unless you already manage it.

Repeat the device steps on another machine with a different deviceId and its own token — token add --label bedroom on the server, then the matching id and token file on the device.

Both devices now feed one shared history, and resume positions derive from the newest event per item across all of them.

Nothing else changes; the labels keep the two devices distinguishable in the stored data.

From anywhere that can reach the server, check liveness first.

Terminal window
curl http://media.lan:7600/healthz

A 200 means the server and its database are up.

Then watch events arrive, using the token of the first device.

Terminal window
curl -H "Authorization: Bearer plt_..." "http://media.lan:7600/v1/events?after=0"

Play something for at least half a minute on the first device, give the sync a moment, and the pull returns the session’s events.

For the cross-device payoff, open the same item on the second device: playback seeks to the freshest saved position and an on-screen line names the device and age it resumed from.

Resume only stages for positions past 30 seconds and under 95 percent of the duration, and only seeks when the saved position leads the live one by more than 10 seconds, so barely-started and nearly-finished items stay untouched.

  • Firefox add-on - track YouTube and Twitch in the browser, on desktop and Android
  • Reading paths - curated sequences through the documentation based on your goals
  • HTTP API - endpoints, authentication, and the event schema