feat(plex): TV-show metadata sync + series filters (Phase 1 of series view)
Give TV shows the same filterable metadata as movies so the TV grid can be filtered/sorted, not just library+sort. Backend: migration 0052 adds rating/content_rating/studio/originally_available_at/genres/directors/cast_names/ people_text to plex_shows (+ GIN/indexes, people_text folded into search_vector); _sync_shows populates them cheaply from the show section listing (no per-item calls). /browse show-branch gains the movie filter set (minus duration) plus an aggregate per-user watch-state (a show rolls up its episodes: all watched=watched, any progress=in_progress, none=new) with year/rating/release sorts; /facets returns show facets. Frontend: PlexSidebar renders the metadata filters + watch-state for TV libraries (duration stays movie-only); show cards show a watched/in-progress badge. i18n plex.inProgress (en/hu/de). Needs a Plex re-sync to populate the new columns.
This commit is contained in:
parent
f0bab315ad
commit
569d31235d
9 changed files with 266 additions and 52 deletions
91
backend/alembic/versions/0052_plex_show_meta.py
Normal file
91
backend/alembic/versions/0052_plex_show_meta.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
"""Plex show-level filterable/orderable metadata
|
||||
|
||||
Revision ID: 0052_plex_show_meta
|
||||
Revises: 0051_plex_link
|
||||
Create Date: 2026-07-10
|
||||
|
||||
Gives TV shows the same filterable metadata movies already have (0045/0046), mirrored cheaply from
|
||||
the Plex show section listing (no per-item calls): rating (audienceRating ~ IMDb), content rating,
|
||||
studio/network, first-air date, and GIN-indexed genre / director / cast arrays + a `people_text`
|
||||
blob folded into the generated `search_vector` (weight B). This lets the TV grid be filtered and
|
||||
sorted like the movie grid. A generated column's expression can't be altered in place, so the
|
||||
search_vector is dropped + recreated. All new columns are populated on the next Plex sync.
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
revision: str = "0052_plex_show_meta"
|
||||
down_revision: Union[str, None] = "0051_plex_link"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
_CFG = "public.unaccent_simple"
|
||||
_NEW_VECTOR = (
|
||||
f"setweight(to_tsvector('{_CFG}', coalesce(title, '')), 'A') || "
|
||||
f"setweight(to_tsvector('{_CFG}', coalesce(people_text, '')), 'B') || "
|
||||
f"setweight(to_tsvector('{_CFG}', left(coalesce(summary, ''), 1000)), 'C')"
|
||||
)
|
||||
_OLD_VECTOR = (
|
||||
f"setweight(to_tsvector('{_CFG}', coalesce(title, '')), 'A') || "
|
||||
f"setweight(to_tsvector('{_CFG}', left(coalesce(summary, ''), 1000)), 'C')"
|
||||
)
|
||||
|
||||
|
||||
def _rebuild_vector(expr: str) -> None:
|
||||
op.execute("DROP INDEX IF EXISTS ix_plex_shows_search_vector")
|
||||
op.execute("ALTER TABLE plex_shows DROP COLUMN search_vector")
|
||||
op.execute(
|
||||
f"ALTER TABLE plex_shows ADD COLUMN search_vector tsvector "
|
||||
f"GENERATED ALWAYS AS ({expr}) STORED"
|
||||
)
|
||||
op.execute("CREATE INDEX ix_plex_shows_search_vector ON plex_shows USING gin (search_vector)")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("plex_shows", sa.Column("rating", sa.Float(), nullable=True))
|
||||
op.add_column("plex_shows", sa.Column("content_rating", sa.String(length=16), nullable=True))
|
||||
op.add_column("plex_shows", sa.Column("studio", sa.String(length=255), nullable=True))
|
||||
op.add_column("plex_shows", sa.Column("originally_available_at", sa.Date(), nullable=True))
|
||||
op.add_column("plex_shows", sa.Column("genres", postgresql.JSONB(), nullable=True))
|
||||
op.add_column("plex_shows", sa.Column("directors", postgresql.JSONB(), nullable=True))
|
||||
op.add_column("plex_shows", sa.Column("cast_names", postgresql.JSONB(), nullable=True))
|
||||
op.add_column("plex_shows", sa.Column("people_text", sa.Text(), nullable=True))
|
||||
|
||||
op.create_index("ix_plex_shows_rating", "plex_shows", ["rating"])
|
||||
op.create_index("ix_plex_shows_content_rating", "plex_shows", ["content_rating"])
|
||||
op.create_index("ix_plex_shows_studio", "plex_shows", ["studio"])
|
||||
op.create_index("ix_plex_shows_originally_available_at", "plex_shows", ["originally_available_at"])
|
||||
op.create_index("ix_plex_shows_genres_gin", "plex_shows", ["genres"], postgresql_using="gin")
|
||||
op.create_index("ix_plex_shows_directors_gin", "plex_shows", ["directors"], postgresql_using="gin")
|
||||
op.create_index("ix_plex_shows_cast_gin", "plex_shows", ["cast_names"], postgresql_using="gin")
|
||||
# NB: ix_plex_shows_collections_gin already exists (created with collections in 0047).
|
||||
|
||||
_rebuild_vector(_NEW_VECTOR)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
_rebuild_vector(_OLD_VECTOR)
|
||||
for ix in (
|
||||
"ix_plex_shows_cast_gin",
|
||||
"ix_plex_shows_directors_gin",
|
||||
"ix_plex_shows_genres_gin",
|
||||
"ix_plex_shows_originally_available_at",
|
||||
"ix_plex_shows_studio",
|
||||
"ix_plex_shows_content_rating",
|
||||
"ix_plex_shows_rating",
|
||||
):
|
||||
op.drop_index(ix, table_name="plex_shows")
|
||||
for col in (
|
||||
"people_text",
|
||||
"cast_names",
|
||||
"directors",
|
||||
"genres",
|
||||
"originally_available_at",
|
||||
"studio",
|
||||
"content_rating",
|
||||
"rating",
|
||||
):
|
||||
op.drop_column("plex_shows", col)
|
||||
Loading…
Add table
Add a link
Reference in a new issue