feat(playlists): remove and move many videos at once, and undo either, in one transaction
This commit is contained in:
@@ -482,6 +482,119 @@ def move_item(db: Session, src: Playlist, dst: Playlist, video_id: str) -> None:
|
||||
db.flush()
|
||||
|
||||
|
||||
def remove_items(db: Session, pl: Playlist, video_ids: list[str]) -> int:
|
||||
"""Remove these videos from a playlist, in one transaction; a video not in it any more is simply
|
||||
not counted. Says how many went."""
|
||||
lock_playlists(db, pl.id)
|
||||
wanted = set(video_ids)
|
||||
rows = [it for it in queries.items(db, pl.id) if it.video_id in wanted]
|
||||
for row in rows:
|
||||
db.delete(row)
|
||||
if rows:
|
||||
recompute_dirty(db, pl)
|
||||
touch(pl)
|
||||
db.flush()
|
||||
return len(rows)
|
||||
|
||||
|
||||
def _insert_at(db: Session, pl: Playlist, placed: list[tuple[int, str]]) -> int:
|
||||
"""Add videos at their indices (ascending, so each index is where the row stood when the rows
|
||||
before it were back) and renumber the list 0..n-1, as positions can have gaps. A video already in
|
||||
the list stays where it is, and one the catalog has lost is left out. Says how many were added;
|
||||
the caller holds the playlist's lock."""
|
||||
rows = queries.items(db, pl.id)
|
||||
present = {r.video_id for r in rows}
|
||||
known = queries.known_video_ids(db, [vid for _, vid in placed])
|
||||
added = 0
|
||||
for at, vid in sorted(placed, key=lambda p: p[0]):
|
||||
if vid in present or vid not in known:
|
||||
continue
|
||||
row = PlaylistItem(playlist_id=pl.id, video_id=vid, position=0)
|
||||
db.add(row)
|
||||
rows.insert(min(at, len(rows)), row)
|
||||
present.add(vid)
|
||||
added += 1
|
||||
for pos, row in enumerate(rows):
|
||||
row.position = pos
|
||||
return added
|
||||
|
||||
|
||||
def restore_items(db: Session, pl: Playlist, video_ids: list[str], positions: list[int]) -> int:
|
||||
"""Put removed videos back at their places, in ONE request: an append plus a whole-list reorder
|
||||
leaves them at the end whenever the second call fails. Says how many came back."""
|
||||
lock_playlists(db, pl.id)
|
||||
added = _insert_at(db, pl, list(zip(positions, video_ids, strict=True)))
|
||||
if added:
|
||||
recompute_dirty(db, pl)
|
||||
touch(pl)
|
||||
db.flush()
|
||||
return added
|
||||
|
||||
|
||||
def move_items(
|
||||
db: Session, src: Playlist, dst: Playlist, video_ids: list[str]
|
||||
) -> tuple[list[str], list[str]]:
|
||||
"""Move these videos from `src` into `dst`, in the source's order whatever order they were picked
|
||||
in, as `move_item` does one. Returns the ones that landed in the target and the ones it already
|
||||
held, which only left the source: the Undo needs to tell them apart. A video a concurrent edit
|
||||
already took out of the source is in neither."""
|
||||
if src.id == dst.id:
|
||||
raise MoveRefused("same_playlist")
|
||||
lock_playlists(db, src.id, dst.id)
|
||||
wanted = set(video_ids)
|
||||
rows = [it for it in queries.items(db, src.id) if it.video_id in wanted]
|
||||
held = queries.in_playlist(db, dst.id, [r.video_id for r in rows])
|
||||
position = next_item_position(db, dst.id)
|
||||
moved: list[str] = []
|
||||
already_there: list[str] = []
|
||||
for row in rows:
|
||||
if row.video_id in held:
|
||||
already_there.append(row.video_id)
|
||||
db.delete(row)
|
||||
else:
|
||||
moved.append(row.video_id)
|
||||
# Re-parented, as `move_item` says why: `added_at` survives the move.
|
||||
row.playlist_id = dst.id
|
||||
row.position = position
|
||||
position += 1
|
||||
if rows:
|
||||
recompute_dirty(db, src)
|
||||
touch(src)
|
||||
if moved:
|
||||
recompute_dirty(db, dst)
|
||||
touch(dst)
|
||||
db.flush()
|
||||
return moved, already_there
|
||||
|
||||
|
||||
def undo_move(
|
||||
db: Session,
|
||||
src: Playlist,
|
||||
dst: Playlist,
|
||||
moved: list[str],
|
||||
already_there: list[str],
|
||||
positions: list[int],
|
||||
) -> int:
|
||||
"""Take a move back in one transaction: the `moved` videos leave `dst` and every video returns to
|
||||
its place in `src`, while the `already_there` ones stay in `dst`, which held them before.
|
||||
`positions` pairs with `moved` + `already_there`. Says how many came back."""
|
||||
lock_playlists(db, src.id, dst.id)
|
||||
wanted = set(moved)
|
||||
rows = [it for it in queries.items(db, dst.id) if it.video_id in wanted]
|
||||
for row in rows:
|
||||
db.delete(row)
|
||||
db.flush()
|
||||
back = _insert_at(db, src, list(zip(positions, [*moved, *already_there], strict=True)))
|
||||
if rows:
|
||||
recompute_dirty(db, dst)
|
||||
touch(dst)
|
||||
if back:
|
||||
recompute_dirty(db, src)
|
||||
touch(src)
|
||||
db.flush()
|
||||
return back
|
||||
|
||||
|
||||
def reorder_items(db: Session, pl: Playlist, order: list[str]) -> int:
|
||||
"""Number the listed videos 0..n in the listed order. Items the list left out (added in another
|
||||
tab meanwhile) keep their relative order after them, so no two items share a position. The
|
||||
|
||||
Reference in New Issue
Block a user