feat(reader): a Google Takeout export becomes watch state and Watch later

The YouTube API reads neither a user's watch history nor their Watch later list, so the Takeout
export is the only way in. `POST /api/takeout` takes one file of it (watch-history.json or
Watch later-videos.csv, told apart by content), keeps its videos in `takeout_items` and queues the
user's `takeout_import` run, whose outcome reaches their inbox.

The run marks watched the known videos the Shorts probe has cleared, at the Takeout time; a Short
is not marked, and a state the user set here (watched, hidden, or changed after that time) wins, by
the upsert's own condition. A video the catalog does not know yet waits with no outcome. Watch later
videos are fetched if unknown and appended oldest first.

An ad-hoc run can now ask for a completion notice (`run_ad_hoc(..., notify=True)`).
This commit is contained in:
2026-10-23 17:04:54 +02:00
parent bf5ceec8a1
commit 7b8505af95
15 changed files with 978 additions and 29 deletions
+3 -1
View File
@@ -249,6 +249,8 @@
"api_token_preset_grants": "The Hub token gets its folder by itself — leave the folders out",
"api_token_no_grants": "Choose at least one folder the token may use",
"api_token_name_required": "Give the token a name",
"api_token_limit": "You already have {{limit}} tokens — revoke one first"
"api_token_limit": "You already have {{limit}} tokens — revoke one first",
"takeout_unreadable": "This isn't a Takeout file the import reads. Upload watch-history.json (JSON format) or Watch later-videos.csv.",
"takeout_no_videos": "This file has no videos to import"
}
}
+13 -4
View File
@@ -80,7 +80,8 @@
"log_gc": "Deletes stored log events older than the retention window (Configuration → Diagnostics). Runs hourly so nothing outlives the promised limit by more than an hour.",
"tmdb_enrich": "Enriches the mirrored Plex catalogue from TMDB (keywords, crew, budget/revenue, TMDb score, recommendations, artwork), matched by each title's canonical id and served from the app's own database. Optional — a no-op until a TMDB API key is set. If it stops, new Plex titles just miss their TMDB extras until it runs again.",
"people_enrich": "Makes the people in the Plex library local: fetches each person's biography from TMDB and caches their portrait, resized through Plex, the most-credited people first. A person page and a cast strip then open from the app's own data. The TMDB half does nothing without a TMDB API key, the portrait half nothing without Plex. If it stops, a person page fetches its own bio when opened, a moment later.",
"channel_backfill": "Turning on full history for a channel fetches its whole back-catalogue at once, as its own run. The Backfill job finishes what this one could not (a very large channel, low quota, or two of these already running)."
"channel_backfill": "Turning on full history for a channel fetches its whole back-catalogue at once, as its own run. The Backfill job finishes what this one could not (a very large channel, low quota, or two of these already running).",
"takeout_import": "A user's Google Takeout import: what they watched on YouTube becomes watched here (not Shorts, never over a state they set here), and their YouTube Watch later list joins theirs. A video the catalogue does not know yet waits, and is marked once it arrives."
},
"jobs": {
"rss_poll": "RSS poll (new uploads)",
@@ -100,7 +101,8 @@
"audit_gc": "Activity and quota-history cleanup",
"log_gc": "Log cleanup",
"people_enrich": "People details and portraits (Plex)",
"channel_backfill": "A channel's full history"
"channel_backfill": "A channel's full history",
"takeout_import": "Google Takeout import"
},
"queue": {
"recentPending": "Channels to sync",
@@ -178,7 +180,13 @@
"deep": "deep",
"bios": "biographies",
"portraits": "portraits",
"cached": "cached"
"cached": "cached",
"kept": "kept as set here",
"waiting": "waiting for their video",
"watch_later_added": "added to Watch later",
"watch_later_present": "already in Watch later",
"watch_later_missing": "no longer on YouTube",
"watch_later_waiting": "Watch later waiting"
},
"skip": {
"disabled": "disabled",
@@ -191,7 +199,8 @@
"busy": "another sync is running",
"channelbusy": "this channel is already being backfilled",
"atcapacity": "two full-history backfills are already running — the scheduler finishes this one",
"nothingtodo": "nothing left to fetch"
"nothingtodo": "nothing left to fetch",
"nouser": "the account is gone"
},
"failure": {
"rss_all_failed_one": "The RSS feed did not answer for the one channel, {{channel}}: {{cause}}. Read through the API instead: {{read}}.",
+3 -1
View File
@@ -249,6 +249,8 @@
"api_token_preset_grants": "A Hub-token magától kapja a mappáját — ne adj meg mappát",
"api_token_no_grants": "Válassz legalább egy mappát, amelyet a token használhat",
"api_token_name_required": "Adj nevet a tokennek",
"api_token_limit": "Már {{limit}} tokened van — előbb vonj vissza egyet"
"api_token_limit": "Már {{limit}} tokened van — előbb vonj vissza egyet",
"takeout_unreadable": "Ez nem olyan Takeout-fájl, amit az import olvas. A watch-history.json (JSON formátum) vagy a Watch later-videos.csv fájlt töltsd fel.",
"takeout_no_videos": "Ebben a fájlban nincs importálható videó"
}
}
+13 -4
View File
@@ -80,7 +80,8 @@
"log_gc": "Törli a megőrzési időnél (Konfiguráció → Diagnosztika) régebbi tárolt naplóeseményeket. Óránként fut, így semmi sem marad meg egy óránál tovább az ígért határidőn túl.",
"tmdb_enrich": "A tükrözött Plex-katalógust dúsítja a TMDB-ből (kulcsszavak, stáb, költségvetés/bevétel, TMDb-pontszám, ajánlások, artwork), a címek kanonikus azonosítója alapján párosítva, és az app saját adatbázisából szolgálja ki. Opcionális — TMDB API-kulcs nélkül nem csinál semmit. Ha leáll, az új Plex-címek csak a következő futásig maradnak TMDB-extrák nélkül.",
"people_enrich": "Helyivé teszi a Plex-könyvtár szereplőit: a TMDB-ből letölti az életrajzukat, a portréjukat pedig a Plexszel kicsinyítve elmenti, a legtöbbet szereplőkkel kezdve. A személyoldal és a szereplősáv így az app saját adataiból nyílik meg. A TMDB-rész TMDB API-kulcs nélkül nem csinál semmit, a portrérész Plex nélkül. Ha leáll, a személyoldal megnyitáskor maga kéri le az életrajzot, egy pillanattal később.",
"channel_backfill": "Ha egy csatornánál bekapcsolod a teljes előzményt, a teljes korábbi kínálata egyszerre, saját futásként töltődik le. Amit ez nem tud befejezni (egy nagyon nagy csatorna, kevés kvóta, vagy ha már két ilyen fut), azt a „Videók behúzása” feladat fejezi be."
"channel_backfill": "Ha egy csatornánál bekapcsolod a teljes előzményt, a teljes korábbi kínálata egyszerre, saját futásként töltődik le. Amit ez nem tud befejezni (egy nagyon nagy csatorna, kevés kvóta, vagy ha már két ilyen fut), azt a „Videók behúzása” feladat fejezi be.",
"takeout_import": "Egy felhasználó Google Takeout importja: amit a YouTube-on megnézett, itt is megnézett lesz (a Shorts nem, és amit itt beállított, azt nem írja felül), a YouTube Watch later listája pedig bekerül az övébe. A katalógusban még nem szereplő videó vár, és amint megérkezik, megnézett lesz."
},
"jobs": {
"rss_poll": "RSS-lekérdezés (új feltöltések)",
@@ -100,7 +101,8 @@
"audit_gc": "Napló- és kvótaelőzmény-takarítás",
"log_gc": "Naplótakarítás",
"people_enrich": "Személyadatok és portrék (Plex)",
"channel_backfill": "Egy csatorna teljes előzménye"
"channel_backfill": "Egy csatorna teljes előzménye",
"takeout_import": "Google Takeout import"
},
"queue": {
"recentPending": "Szinkronra váró csatorna",
@@ -178,7 +180,13 @@
"deep": "mély",
"bios": "életrajzok",
"portraits": "portrék",
"cached": "elmentve"
"cached": "elmentve",
"kept": "itteni állapot megtartva",
"waiting": "a videójára vár",
"watch_later_added": "Watch laterbe téve",
"watch_later_present": "már a Watch laterben",
"watch_later_missing": "már nincs a YouTube-on",
"watch_later_waiting": "Watch later vár"
},
"skip": {
"disabled": "kikapcsolva",
@@ -191,7 +199,8 @@
"busy": "egy másik szinkron fut",
"channelbusy": "ennek a csatornának a betöltése már fut",
"atcapacity": "már két teljes előzmény-betöltés fut — ezt az ütemező fejezi be",
"nothingtodo": "nincs több letöltendő"
"nothingtodo": "nincs több letöltendő",
"nouser": "a fiók már nem létezik"
},
"failure": {
"rss_all_failed_one": "Az RSS-hírcsatorna az egyetlen csatornánál sem válaszolt ({{channel}}): {{cause}}. Az API-n át beolvasva helyette: {{read}}.",