feat(settings): upload a Google Takeout export in Settings › Account

A "YouTube history" section under YouTube access: the watch history and
the Watch later csv are dropped or picked (one request each, each file's
outcome said on its own), and the section then says what the import did
- only the counts it produced - or that it is running, failed or was cut
off by a restart. While it runs it asks again until it ends; a failed
re-read shows the failure, not the previous answer.

The drop zone the Files upload had inline is now ui/DropZone, used by
both. The e2e seed clears the account's kept Takeout videos.
This commit is contained in:
2026-10-23 18:51:05 +02:00
parent 5d9952ab1e
commit 3553eac8c4
16 changed files with 749 additions and 84 deletions
+4 -1
View File
@@ -28,7 +28,7 @@ from app.db import SessionLocal
import app.tables # noqa: F401 — every table, before the first query
from app.catalog.models import Channel, Subscription, Video
from app.models import AppState, User
from app.reader.models import Playlist, PlaylistItem, VideoState
from app.reader.models import Playlist, PlaylistItem, TakeoutItem, VideoState
from app.security import hash_password
EMAIL = os.environ.get("E2E_EMAIL", "e2e@siftlode.local")
@@ -118,6 +118,9 @@ def _seed_account(db, email: str, role: str, display_name: str) -> dict:
# - one WATCHED video from the second channel, so the "watched" show-filter is a
# real, non-empty narrowing of the feed (and the "all" vs "unwatched" split exists).
db.execute(delete(VideoState).where(VideoState.user_id == user.id))
# A Takeout import's kept videos too: a settled row keeps its outcome over a re-upload, so the
# import spec's counts would otherwise be the previous run's, not this one's.
db.execute(delete(TakeoutItem).where(TakeoutItem.user_id == user.id))
def _newest(channel_id: str, exclude: set[str]) -> Video | None:
conds = [