i'm the developer of rkmovies, a 0.91 MB 4K media player for Android and Windows. no accounts, no telemetry, no cloud sync. which raises an obvious question: how does "continue watching" work if there's nowhere to sync to?
answer: a composite key in indexeddb, one per profile.
the key
const hkey = (dp, se, ep) => PROFILE + "|" + dp + "|" + se + "|" + ep;
PROFILE is the active profile id (stored in localStorage as rk_profile, default p1). dp is the detail path of the title, se/ep are season and episode (0/0 for movies). so watching episode 4 of something on profile 2 stores a record under p2|/detail/xyz|1|4.
the hist object store uses keyPath: "key", and each record also carries pid, detailPath, se, ep, pos (seconds), dur, watched (0/1) and tot (total episodes) as plain fields. that redundancy is deliberate: i can list "all history for this profile" with one getAll() plus a filter, instead of range-scanning the composite key.
the resume rule
on load:
if (rec && !rec.watched && rec.pos > 10) { art.seek = rec.pos; toast("Resumed at " + fmtTime(rec.pos)); }
the pos > 10 check skips the intros people always skip anyway. watched flips to 1 once you're within the end threshold (or when the "up next" countdown fires), and a seek back below the threshold flips it back and cancels the countdown. no server ever sees any of this.
the part that bit me
i originally kept history in localStorage. fine for one profile, but a family phone with a kid profile needs isolation, and localStorage keys got messy fast. the migration is one flag per profile:
const flag = "rk_idb_migrated_" + PROFILE;
read the old localStorage array once, bulk-put into hist with the new composite keys, set the flag, never look back. profiles are just rows filtered by pid, so switching profiles is a filter change, not a reload.
trade-offs, honestly
- no cross-device resume. phone to tv means re-finding the episode. that's the price of zero accounts, and my users seem fine with it.
- storage grows forever until you clear history per title (
histRemoveTitledeletes by key list). i didn't build auto-pruning. records are a few hundred bytes each, so it takes a lot of watching to matter. - indexeddb can be wiped by "clear site data", same as localStorage. nothing to do about that.
the whole thing is ~80 lines in app-core.js. sometimes the boring local database is the whole feature.
the app is a sub-1 MB media player with hardware decode, multi-audio tracks and webvtt subs. grab the install page for Android or the desktop build for Windows.
Top comments (0)