feat(playlists): unify Saved into a built-in Watch later playlist
The old per-video status='saved' becomes membership in a built-in, undeletable 'watch_later' playlist. Migration 0012 moves existing saved videos into each user's Watch later list and demotes the states to 'new'. The feed/playlist serializers now expose a 'saved' boolean (EXISTS in watch_later); the card bookmark toggles watch_later via new /api/playlists/watch-later add/remove endpoints (create-on-demand) instead of setting a status. Removed the 'saved' show-filter and status. Watch later shows a localized name and hides rename/delete in the Playlists page and add-to-playlist popover.
This commit is contained in:
parent
86844b0bdd
commit
2f66196816
12 changed files with 260 additions and 36 deletions
|
|
@ -7,13 +7,24 @@ from sqlalchemy.orm import Session, aliased
|
|||
from app import quota
|
||||
from app.auth import current_user
|
||||
from app.db import get_db
|
||||
from app.models import Channel, ChannelTag, Subscription, Tag, User, Video, VideoState
|
||||
from app.models import (
|
||||
Channel,
|
||||
ChannelTag,
|
||||
Playlist,
|
||||
PlaylistItem,
|
||||
Subscription,
|
||||
Tag,
|
||||
User,
|
||||
Video,
|
||||
VideoState,
|
||||
)
|
||||
from app.sync.videos import parse_iso8601_duration
|
||||
from app.youtube.client import YouTubeClient, YouTubeError
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["feed"])
|
||||
|
||||
VALID_STATES = {"new", "watched", "saved", "hidden"}
|
||||
# "saved" used to be a status; it's now membership in the built-in Watch later playlist.
|
||||
VALID_STATES = {"new", "watched", "hidden"}
|
||||
HIDDEN_LIVE = ("live", "upcoming")
|
||||
|
||||
# Resume-position thresholds (mirror the client): positions below this are "didn't
|
||||
|
|
@ -29,6 +40,23 @@ def _channel_url(channel_id: str, handle: str | None) -> str:
|
|||
return f"https://www.youtube.com/channel/{channel_id}"
|
||||
|
||||
|
||||
def _saved_expr(user: User):
|
||||
"""A correlated EXISTS labelled `saved`: is this video in the user's Watch later list?
|
||||
(Watch later is the built-in playlist that replaced the old per-video 'saved' status.)"""
|
||||
return (
|
||||
select(1)
|
||||
.select_from(PlaylistItem)
|
||||
.join(Playlist, Playlist.id == PlaylistItem.playlist_id)
|
||||
.where(
|
||||
Playlist.user_id == user.id,
|
||||
Playlist.kind == "watch_later",
|
||||
PlaylistItem.video_id == Video.id,
|
||||
)
|
||||
.exists()
|
||||
.label("saved")
|
||||
)
|
||||
|
||||
|
||||
def _serialize(row) -> dict:
|
||||
return {
|
||||
"id": row.id,
|
||||
|
|
@ -45,6 +73,7 @@ def _serialize(row) -> dict:
|
|||
"live_status": row.live_status,
|
||||
"status": row.status or "new",
|
||||
"position_seconds": row.position_seconds or 0,
|
||||
"saved": bool(getattr(row, "saved", False)),
|
||||
"watch_url": f"https://www.youtube.com/watch?v={row.id}",
|
||||
}
|
||||
|
||||
|
|
@ -96,6 +125,7 @@ def _filtered_query(
|
|||
Video.live_status,
|
||||
status_expr.label("status"),
|
||||
position_expr.label("position_seconds"),
|
||||
_saved_expr(user),
|
||||
).join(Channel, Channel.id == Video.channel_id)
|
||||
|
||||
if scope == "all":
|
||||
|
|
@ -176,7 +206,7 @@ def _filtered_query(
|
|||
|
||||
# Content type: Normal / Shorts / Live·Upcoming as a union of enabled types.
|
||||
# Explicit Watched/Saved/Hidden views show every type so nothing goes missing.
|
||||
explicit_view = show in ("watched", "saved", "hidden")
|
||||
explicit_view = show in ("watched", "hidden")
|
||||
if not explicit_view:
|
||||
type_clauses = []
|
||||
if show_normal:
|
||||
|
|
@ -198,8 +228,6 @@ def _filtered_query(
|
|||
)
|
||||
elif show == "watched":
|
||||
query = query.where(status_expr == "watched")
|
||||
elif show == "saved":
|
||||
query = query.where(status_expr == "saved")
|
||||
elif show == "hidden":
|
||||
query = query.where(status_expr == "hidden")
|
||||
else: # all
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue