Plex, library and /watch tell the operating system what plays (title, line, picture) and take its
play / pause / skip / seek through one hook (lib/mediaSession); the Plex player drives it with its own
seek and clock, so the lock screen shows the film's position, not the stream's. The YouTube player is
left alone: its iframe already publishes all of it, and the OS routes to the frame that plays.
The library and /watch players get the basic keys (lib/mediaKeys, the keymap's mediaPlayer rows) and
open where the viewer stopped (lib/resume): the library keeps the point on the server, per user and
library item (library_states, POST /api/downloads/{id}/progress, listed back on both listings), the
shared link in this browser. A started library row continues, its menu can start it over, and its
picture carries the cards' progress bar. The resume rule (too early / near the end = from the start)
is one function shared with the YouTube route.
The library dialog opens focused on its panel (Modal initialFocus "player"): on the ✕, Space closed
the film. While the media element itself has the focus, Space and the arrows stay its own controls'
— the browser acts on them whatever a listener does, and one Space played and paused.
43 lines
1.4 KiB
Python
43 lines
1.4 KiB
Python
"""library: a resume point per user and library item
|
|
|
|
The library player opened every item at 0:00. `library_states` keeps where a user stopped, per job
|
|
(their own download, or one shared with them) — the same shape as `video_states`' position, without a
|
|
status: the library has no "watched". It starts empty; the downgrade drops it.
|
|
"""
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
revision: str = "0099_library_states"
|
|
down_revision: str | None = "0098_drop_demo_account"
|
|
branch_labels: str | None = None
|
|
depends_on: str | None = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
"library_states",
|
|
sa.Column("id", sa.Integer(), primary_key=True),
|
|
sa.Column(
|
|
"user_id",
|
|
sa.Integer(),
|
|
sa.ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"job_id",
|
|
sa.Integer(),
|
|
sa.ForeignKey("download_jobs.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column("position_seconds", sa.Integer(), nullable=False),
|
|
sa.Column("progress_updated_at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.UniqueConstraint("user_id", "job_id", name="uq_library_user_job"),
|
|
)
|
|
op.create_index("ix_library_states_job_id", "library_states", ["job_id"])
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_index("ix_library_states_job_id", table_name="library_states")
|
|
op.drop_table("library_states")
|