"""Demo-account service: reset the shared demo sandbox to a clean, seeded baseline. Shared by the admin's manual reset endpoint (routes/admin.py) and the scheduled demo-reset job (scheduler.py). Lives in the service layer, not routes, so the scheduler can call it without an `app.routes -> app.scheduler` import cycle (the reason its import used to be lazy). """ from sqlalchemy import delete, func, select from sqlalchemy.orm import Session from app.models import Playlist, PlaylistItem, Subscription, User, Video, VideoState def _seed_demo_playlists(db: Session, demo: User) -> int: """Seed a few sample playlists from the shared catalog so a freshly-reset demo isn't empty — gives visitors something to play with. Best-effort: skips silently if the catalog is too small.""" recent = ( db.execute( select(Video.id) .where(Video.is_short.is_(False), Video.live_status == "none") .order_by(Video.published_at.desc().nullslast()) .limit(60) ) .scalars() .all() ) if not recent: return 0 samples = [ ("Fresh picks", recent[0:10]), ("Quick watch", recent[10:18]), ("Saved for later", recent[18:26]), ] created = 0 for pos, (name, vids) in enumerate(samples): if not vids: continue pl = Playlist(user_id=demo.id, name=name, position=pos) db.add(pl) db.flush() for ipos, vid in enumerate(vids): db.add(PlaylistItem(playlist_id=pl.id, video_id=vid, position=ipos)) created += 1 return created def _seed_demo_subscriptions(db: Session, demo: User, n: int = 14) -> int: """Seed the demo with a handful of catalog channels (the ones with the most videos) so its Channel manager isn't empty — gives visitors something to explore and matches the landing-page screenshot. Idempotent: clears the demo's subscriptions first, then re-adds the top N.""" db.execute(delete(Subscription).where(Subscription.user_id == demo.id)) top = ( db.execute( select(Video.channel_id) .where(Video.channel_id.is_not(None)) .group_by(Video.channel_id) .order_by(func.count().desc()) .limit(n) ) .scalars() .all() ) for channel_id in top: db.add(Subscription(user_id=demo.id, channel_id=channel_id)) return len(top) def reset_demo_state(db: Session, demo: User) -> int: """Wipe the demo account's per-user state (watch/save/hide states, playlists, preferences) back to a clean baseline and re-seed a few sample playlists + channel subscriptions. Returns the seeded playlist count. Shared by the admin's manual reset button and the scheduled demo reset (see scheduler).""" db.execute(delete(VideoState).where(VideoState.user_id == demo.id)) # Playlist items cascade on playlist delete (ondelete="CASCADE"). db.execute(delete(Playlist).where(Playlist.user_id == demo.id)) demo.preferences = {"language": "en"} db.commit() seeded = _seed_demo_playlists(db, demo) _seed_demo_subscriptions(db, demo) db.commit() return seeded