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
|
|
@ -8,7 +8,7 @@ from sqlalchemy.orm import Session, aliased
|
|||
from app.auth import current_user
|
||||
from app.db import get_db
|
||||
from app.models import Channel, Playlist, PlaylistItem, User, Video, VideoState
|
||||
from app.routes.feed import _serialize
|
||||
from app.routes.feed import _saved_expr, _serialize
|
||||
|
||||
router = APIRouter(prefix="/api/playlists", tags=["playlists"])
|
||||
|
||||
|
|
@ -110,6 +110,83 @@ def create_playlist(
|
|||
return _summary(db, pl, 0)
|
||||
|
||||
|
||||
def _watch_later(db: Session, user: User) -> Playlist:
|
||||
"""The user's built-in Watch later playlist, created on first use."""
|
||||
pl = db.scalar(
|
||||
select(Playlist).where(
|
||||
Playlist.user_id == user.id, Playlist.kind == "watch_later"
|
||||
)
|
||||
)
|
||||
if pl is None:
|
||||
next_pos = (
|
||||
db.scalar(
|
||||
select(func.coalesce(func.max(Playlist.position), -1)).where(
|
||||
Playlist.user_id == user.id
|
||||
)
|
||||
)
|
||||
+ 1
|
||||
)
|
||||
pl = Playlist(
|
||||
user_id=user.id, name="Watch later", kind="watch_later", position=next_pos
|
||||
)
|
||||
db.add(pl)
|
||||
db.commit()
|
||||
return pl
|
||||
|
||||
|
||||
@router.post("/watch-later")
|
||||
def add_watch_later(
|
||||
payload: dict,
|
||||
user: User = Depends(current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""Bookmark shortcut: add a video to the built-in Watch later playlist."""
|
||||
video_id = payload.get("video_id")
|
||||
if not video_id or db.get(Video, video_id) is None:
|
||||
raise HTTPException(status_code=404, detail="Unknown video")
|
||||
pl = _watch_later(db, user)
|
||||
exists = db.scalar(
|
||||
select(PlaylistItem).where(
|
||||
PlaylistItem.playlist_id == pl.id, PlaylistItem.video_id == video_id
|
||||
)
|
||||
)
|
||||
if exists is None:
|
||||
next_pos = (
|
||||
db.scalar(
|
||||
select(func.coalesce(func.max(PlaylistItem.position), -1)).where(
|
||||
PlaylistItem.playlist_id == pl.id
|
||||
)
|
||||
)
|
||||
+ 1
|
||||
)
|
||||
db.add(PlaylistItem(playlist_id=pl.id, video_id=video_id, position=next_pos))
|
||||
db.commit()
|
||||
return {"saved": True, "playlist_id": pl.id}
|
||||
|
||||
|
||||
@router.delete("/watch-later/{video_id}")
|
||||
def remove_watch_later(
|
||||
video_id: str,
|
||||
user: User = Depends(current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
pl = db.scalar(
|
||||
select(Playlist).where(
|
||||
Playlist.user_id == user.id, Playlist.kind == "watch_later"
|
||||
)
|
||||
)
|
||||
if pl is not None:
|
||||
item = db.scalar(
|
||||
select(PlaylistItem).where(
|
||||
PlaylistItem.playlist_id == pl.id, PlaylistItem.video_id == video_id
|
||||
)
|
||||
)
|
||||
if item is not None:
|
||||
db.delete(item)
|
||||
db.commit()
|
||||
return {"saved": False}
|
||||
|
||||
|
||||
@router.get("/{playlist_id}")
|
||||
def get_playlist(
|
||||
playlist_id: int,
|
||||
|
|
@ -134,6 +211,7 @@ def get_playlist(
|
|||
Video.live_status,
|
||||
func.coalesce(state.status, "new").label("status"),
|
||||
func.coalesce(state.position_seconds, 0).label("position_seconds"),
|
||||
_saved_expr(user),
|
||||
)
|
||||
.join(PlaylistItem, PlaylistItem.video_id == Video.id)
|
||||
.join(Channel, Channel.id == Video.channel_id)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue