feat(playlists): backend foundation — model, migration, CRUD API
Add per-user local playlists: Playlist + PlaylistItem models (with forward-looking kind/source/yt_playlist_id/dirty columns for the later YouTube-sync phases) and migration 0011. New /api/playlists routes: list (with optional contains=<video_id> membership flags for the add-to-playlist popover), create, get detail (items serialized via the feed serializer, with per-user watch state), rename, delete, add/remove item, and reorder. Watch later (built-in) is protected from deletion.
This commit is contained in:
parent
2ec1887abf
commit
da9dcf93d5
4 changed files with 412 additions and 1 deletions
|
|
@ -301,3 +301,64 @@ class AppState(Base):
|
|||
sync_paused: Mapped[bool] = mapped_column(
|
||||
Boolean, default=False, server_default="false"
|
||||
)
|
||||
|
||||
|
||||
class Playlist(Base):
|
||||
"""A per-user named, ordered collection of videos from the shared catalog.
|
||||
|
||||
`kind`: 'user' (normal) or 'watch_later' (built-in, undeletable, one per user).
|
||||
`source`: 'local' (created here) or 'youtube' (mirrored from the user's YouTube
|
||||
playlists). `yt_playlist_id` links a mirrored/exported playlist to its YouTube id.
|
||||
`dirty` marks a YouTube-linked playlist with local edits not yet pushed, so the read
|
||||
sync won't clobber them. (source/yt_playlist_id/dirty are forward-looking for the YT
|
||||
sync phases; the foundation phase only uses local playlists.)"""
|
||||
|
||||
__tablename__ = "playlists"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(255))
|
||||
kind: Mapped[str] = mapped_column(String(16), default="user", server_default="user")
|
||||
source: Mapped[str] = mapped_column(
|
||||
String(16), default="local", server_default="local"
|
||||
)
|
||||
yt_playlist_id: Mapped[str | None] = mapped_column(String(64))
|
||||
dirty: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
|
||||
position: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
items: Mapped[list["PlaylistItem"]] = relationship(
|
||||
back_populates="playlist",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="PlaylistItem.position",
|
||||
)
|
||||
|
||||
|
||||
class PlaylistItem(Base):
|
||||
"""A video's membership in a playlist, with its order within that playlist."""
|
||||
|
||||
__tablename__ = "playlist_items"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("playlist_id", "video_id", name="uq_playlist_video"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
playlist_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("playlists.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
video_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("videos.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
position: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
|
||||
added_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
)
|
||||
|
||||
playlist: Mapped["Playlist"] = relationship(back_populates="items")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue