feat(playlists): rail sorting, consolidated item sort, persist selection
1) Fix: the selected playlist is now persisted (localStorage) and scrolled into view, so F5 keeps it instead of jumping to the first one. 2) Consolidate the in-detail sort: one key select (Title/Duration/Channel) + an asc/desc direction toggle (was separate A-Z / Z-A / shortest / longest options); add Channel as a sort key. Direction also orders the channel groups when grouping. 3) Left rail sorting: by name / item count / total length, asc/desc, plus an 'unsynced first' toggle that floats playlists with unpushed edits to the top. Backend: list/summary now return total_duration_seconds (grouped sum). The rail also shows each playlist's total length. Sort prefs persist to localStorage.
This commit is contained in:
parent
ac73725735
commit
740a14af4b
6 changed files with 237 additions and 57 deletions
|
|
@ -31,7 +31,13 @@ def _mark_dirty(pl: Playlist) -> None:
|
|||
pl.dirty = True
|
||||
|
||||
|
||||
def _summary(db: Session, pl: Playlist, count: int, has_video: bool | None = None) -> dict:
|
||||
def _summary(
|
||||
db: Session,
|
||||
pl: Playlist,
|
||||
count: int,
|
||||
total_duration: int = 0,
|
||||
has_video: bool | None = None,
|
||||
) -> dict:
|
||||
cover = db.scalar(
|
||||
select(Video.thumbnail_url)
|
||||
.join(PlaylistItem, PlaylistItem.video_id == Video.id)
|
||||
|
|
@ -47,6 +53,7 @@ def _summary(db: Session, pl: Playlist, count: int, has_video: bool | None = Non
|
|||
"yt_playlist_id": pl.yt_playlist_id,
|
||||
"dirty": pl.dirty,
|
||||
"item_count": count,
|
||||
"total_duration_seconds": total_duration,
|
||||
"cover_thumbnail": cover,
|
||||
}
|
||||
if has_video is not None:
|
||||
|
|
@ -54,6 +61,18 @@ def _summary(db: Session, pl: Playlist, count: int, has_video: bool | None = Non
|
|||
return out
|
||||
|
||||
|
||||
def _total_duration(db: Session, playlist_id: int) -> int:
|
||||
return (
|
||||
db.scalar(
|
||||
select(func.coalesce(func.sum(Video.duration_seconds), 0))
|
||||
.select_from(PlaylistItem)
|
||||
.join(Video, Video.id == PlaylistItem.video_id)
|
||||
.where(PlaylistItem.playlist_id == playlist_id)
|
||||
)
|
||||
or 0
|
||||
)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_playlists(
|
||||
contains: str | None = None,
|
||||
|
|
@ -81,6 +100,17 @@ def list_playlists(
|
|||
.group_by(PlaylistItem.playlist_id)
|
||||
).all()
|
||||
)
|
||||
durations = dict(
|
||||
db.execute(
|
||||
select(
|
||||
PlaylistItem.playlist_id,
|
||||
func.coalesce(func.sum(Video.duration_seconds), 0),
|
||||
)
|
||||
.join(Video, Video.id == PlaylistItem.video_id)
|
||||
.where(PlaylistItem.playlist_id.in_(ids))
|
||||
.group_by(PlaylistItem.playlist_id)
|
||||
).all()
|
||||
)
|
||||
member: set[int] = set()
|
||||
if contains:
|
||||
member = set(
|
||||
|
|
@ -94,7 +124,13 @@ def list_playlists(
|
|||
.all()
|
||||
)
|
||||
return [
|
||||
_summary(db, p, counts.get(p.id, 0), (p.id in member) if contains else None)
|
||||
_summary(
|
||||
db,
|
||||
p,
|
||||
counts.get(p.id, 0),
|
||||
durations.get(p.id, 0),
|
||||
(p.id in member) if contains else None,
|
||||
)
|
||||
for p in pls
|
||||
]
|
||||
|
||||
|
|
@ -339,7 +375,7 @@ def rename_playlist(
|
|||
count = db.scalar(
|
||||
select(func.count()).where(PlaylistItem.playlist_id == pl.id)
|
||||
)
|
||||
return _summary(db, pl, count or 0)
|
||||
return _summary(db, pl, count or 0, _total_duration(db, pl.id))
|
||||
|
||||
|
||||
@router.delete("/{playlist_id}")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue