DEV Community

hammi
hammi

Posted on

how i made resume-anywhere work without a single account (indexeddb key design)

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;
Enter fullscreen mode Exit fullscreen mode

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)); }
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 (histRemoveTitle deletes 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)