feat(playlists): remove and move many videos at once, and undo either, in one transaction
This commit is contained in:
@@ -6,7 +6,7 @@ out (`/{id}/push-plan`, `/{id}/revert-youtube`), behind the optional write scope
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import quota
|
||||
@@ -459,6 +459,124 @@ def move_item(
|
||||
return {"ok": True, "from_playlist_id": src.id, "to_playlist_id": dst.id, "video_id": video_id}
|
||||
|
||||
|
||||
class VideoIdsIn(BaseModel):
|
||||
video_ids: list[str] = []
|
||||
|
||||
|
||||
def _paired(video_count: int, positions: list[int]) -> None:
|
||||
"""One place per video, none before the start: both undo bodies come back from the client."""
|
||||
if len(positions) != video_count:
|
||||
raise ValueError("positions must pair one-to-one with the videos")
|
||||
if any(at < 0 for at in positions):
|
||||
raise ValueError("positions cannot be negative")
|
||||
|
||||
|
||||
class RestoreItemsIn(VideoIdsIn):
|
||||
# Where each video goes back: an index over the ordered rows, one per video.
|
||||
positions: list[int] = []
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _one_position_per_video(self):
|
||||
_paired(len(self.video_ids), self.positions)
|
||||
return self
|
||||
|
||||
|
||||
class MoveItemsIn(VideoIdsIn):
|
||||
target_playlist_id: int
|
||||
|
||||
|
||||
class UndoMoveIn(BaseModel):
|
||||
from_playlist_id: int
|
||||
moved: list[str] = []
|
||||
already_there: list[str] = []
|
||||
# One per video of `moved` + `already_there`, in that order.
|
||||
positions: list[int] = []
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _one_position_per_video(self):
|
||||
_paired(len(self.moved) + len(self.already_there), self.positions)
|
||||
return self
|
||||
|
||||
|
||||
def _other_list(db: Session, user: User, playlist_id: int) -> "Playlist":
|
||||
"""The other list of a move. Its own reason: "this playlist doesn't exist any more" about a list
|
||||
that is gone was read as the one on screen, which is still there."""
|
||||
other = queries.own_playlist(db, user, playlist_id)
|
||||
if other is None:
|
||||
raise not_found("move_target")
|
||||
return other
|
||||
|
||||
|
||||
@router.post("/{playlist_id}/items/remove-bulk")
|
||||
def remove_items(
|
||||
playlist_id: int,
|
||||
body: VideoIdsIn,
|
||||
user: User = Depends(current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""Remove many videos from a playlist at once. Returns how many went."""
|
||||
pl = _own_playlist(db, user, playlist_id)
|
||||
removed = service.remove_items(db, pl, body.video_ids)
|
||||
db.commit()
|
||||
return {"ok": True, "playlist_id": pl.id, "removed": removed}
|
||||
|
||||
|
||||
@router.post("/{playlist_id}/items/restore")
|
||||
def restore_items(
|
||||
playlist_id: int,
|
||||
body: RestoreItemsIn,
|
||||
user: User = Depends(current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""The "Undo" of a removal: the videos back at their places. Returns how many came back."""
|
||||
pl = _own_playlist(db, user, playlist_id)
|
||||
added = service.restore_items(db, pl, body.video_ids, body.positions)
|
||||
db.commit()
|
||||
return {"ok": True, "playlist_id": pl.id, "added": added}
|
||||
|
||||
|
||||
@router.post("/{playlist_id}/items/move-bulk")
|
||||
def move_items(
|
||||
playlist_id: int,
|
||||
body: MoveItemsIn,
|
||||
user: User = Depends(current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""Move many videos into another of the caller's playlists, in one transaction, for the reason
|
||||
`move_item` gives. Says which landed there and which it already held, for the "Undo"."""
|
||||
src = _own_playlist(db, user, playlist_id)
|
||||
dst = _other_list(db, user, body.target_playlist_id)
|
||||
try:
|
||||
moved, already_there = service.move_items(db, src, dst, body.video_ids)
|
||||
except service.MoveRefused as e:
|
||||
raise HTTPException(
|
||||
status_code=400, detail={"code": "playlist_move", "reason": e.reason}
|
||||
) from None
|
||||
db.commit()
|
||||
return {
|
||||
"ok": True,
|
||||
"from_playlist_id": src.id,
|
||||
"to_playlist_id": dst.id,
|
||||
"moved": moved,
|
||||
"already_there": already_there,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{playlist_id}/items/undo-move")
|
||||
def undo_move(
|
||||
playlist_id: int,
|
||||
body: UndoMoveIn,
|
||||
user: User = Depends(current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""The "Undo" of a move, back into this playlist in one transaction. Returns how many came back."""
|
||||
src = _own_playlist(db, user, playlist_id)
|
||||
dst = _other_list(db, user, body.from_playlist_id)
|
||||
added = service.undo_move(db, src, dst, body.moved, body.already_there, body.positions)
|
||||
db.commit()
|
||||
return {"ok": True, "playlist_id": src.id, "added": added}
|
||||
|
||||
|
||||
class ReorderItemsIn(BaseModel):
|
||||
video_ids: list[str] = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user