diff --git a/alembic/versions/20260902_0034_favorite_sites.py b/alembic/versions/20260902_0034_favorite_sites.py new file mode 100644 index 0000000..fa75c81 --- /dev/null +++ b/alembic/versions/20260902_0034_favorite_sites.py @@ -0,0 +1,34 @@ +"""ulubione strony: przypinanie zrodel na gorze zakladki Sites + +Prosba z apki 2026-09-02. Tabela jak favorite_studios, kluczowana po device_id, wiec +kod synchronizacji (= device_id) przenosi ja na inne urzadzenie razem z reszta. +Origin bez klucza obcego: zrodla nie maja tabeli kluczowanej po origin. + +Revision ID: 0034_favorite_sites +Revises: 0033_effective_date_idx +Create Date: 2026-09-02 +""" +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + +revision = "0034_favorite_sites" +down_revision = "0033_effective_date_idx" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "favorite_sites", + sa.Column("device_id", sa.String(64), primary_key=True), + sa.Column("origin", sa.String(64), primary_key=True), + sa.Column( + "created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False + ), + ) + + +def downgrade() -> None: + op.drop_table("favorite_sites") diff --git a/app/api/favorites.py b/app/api/favorites.py index dd77635..d6221b2 100644 --- a/app/api/favorites.py +++ b/app/api/favorites.py @@ -25,7 +25,7 @@ import uuid from datetime import UTC, datetime from typing import Annotated -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, Path, status from pydantic import BaseModel from sqlalchemy import exists, func, select from sqlalchemy.orm import Session @@ -35,6 +35,7 @@ from app.auth import require_api_key from app.db import get_session from app.models.favorite_movie import FavoriteMovie from app.models.favorite_performer import FavoritePerformer +from app.models.favorite_site import FavoriteSite from app.models.favorite_studio import FavoriteStudio from app.models.movie import Movie from app.models.performer import Performer @@ -442,6 +443,70 @@ def mark_studio_seen( return SeenStudioOut(studio_id=studio_id, last_seen_at=fav.last_seen_at) +# ---------- Sites (origins) ---------- +# Przypiete strony w zakladce Sites (prosba z apki 2026-09-02). Bez licznikow "+N": lista +# zrodel i tak pokazuje swiezosc per strona, a badge liczy sie tylko dla performerow, +# studiow i filmow. + +_ORIGIN_RE = r"^[a-z0-9][a-z0-9:._-]{1,63}$" + + +class FavoriteSiteListOut(BaseModel): + items: list[str] # origins, w kolejnosci dodania + total: int + + +class FavoriteSiteAddOut(BaseModel): + origin: str + created: bool + + +@router.get("/sites", response_model=FavoriteSiteListOut) +def list_favorite_sites( + session: Annotated[Session, Depends(get_session)], + device_id: Annotated[str, Depends(get_device_id)], +) -> FavoriteSiteListOut: + rows = session.execute( + select(FavoriteSite.origin) + .where(FavoriteSite.device_id == device_id) + .order_by(FavoriteSite.created_at) + ).scalars().all() + return FavoriteSiteListOut(items=list(rows), total=len(rows)) + + +@router.post("/sites/{origin}", response_model=FavoriteSiteAddOut) +def add_favorite_site( + origin: Annotated[str, Path(pattern=_ORIGIN_RE)], + session: Annotated[Session, Depends(get_session)], + device_id: Annotated[str, Depends(get_device_id)], +) -> FavoriteSiteAddOut: + from app.models.playback_source import PlaybackSource + + znany = session.execute( + select(1).where(PlaybackSource.origin == origin).limit(1) + ).scalar_one_or_none() + if znany is None: + raise HTTPException(status_code=404, detail="unknown origin") + if session.get(FavoriteSite, (device_id, origin)) is not None: + return FavoriteSiteAddOut(origin=origin, created=False) + session.add(FavoriteSite(device_id=device_id, origin=origin)) + session.commit() + return FavoriteSiteAddOut(origin=origin, created=True) + + +@router.delete("/sites/{origin}", status_code=status.HTTP_204_NO_CONTENT) +def remove_favorite_site( + origin: Annotated[str, Path(pattern=_ORIGIN_RE)], + session: Annotated[Session, Depends(get_session)], + device_id: Annotated[str, Depends(get_device_id)], +) -> None: + fav = session.get(FavoriteSite, (device_id, origin)) + if fav is None: + return + session.delete(fav) + session.commit() + + # ── Favorite movies ──────────────────────────────────────────────────────── # Movies nie mają child scenes per-favorite (jak performerki/studia), więc # `last_seen_at` nie jest tu używany do NEW count, tylko jako tracking ostatniego diff --git a/app/models/__init__.py b/app/models/__init__.py index 1b360dc..bd6987d 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -5,6 +5,7 @@ from app.models.external_record import ExternalRecord from app.models.favorite_movie import FavoriteMovie from app.models.favorite_performer import FavoritePerformer from app.models.favorite_scene import FavoriteScene +from app.models.favorite_site import FavoriteSite from app.models.favorite_studio import FavoriteStudio from app.models.ingest_run import IngestRun from app.models.merge_candidate import MergeCandidate @@ -46,6 +47,7 @@ __all__ = [ "FavoriteMovie", "FavoritePerformer", "FavoriteScene", + "FavoriteSite", "FavoriteStudio", "IngestRun", "MergeCandidate", diff --git a/app/models/favorite_site.py b/app/models/favorite_site.py new file mode 100644 index 0000000..0ef461c --- /dev/null +++ b/app/models/favorite_site.py @@ -0,0 +1,25 @@ +"""Ulubione strony (origins), single-user, in-app. Mirror FavoriteStudio struktury. + +Prosba z apki 2026-09-02: przypiac wybrane strony na gorze zakladki Sites. Po stronie +serwera, a nie w SecureStore, bo reszta ulubionych tez jest tu i przechodzi na inne +urzadzenie kodem synchronizacji (kod = device_id). Origin to zwykly napis +(`tube:hqpornercom`), bez FK: zrodla nie maja tabeli kluczowanej po origin. +""" +from __future__ import annotations + +from datetime import datetime + +from sqlalchemy import DateTime, String, func +from sqlalchemy.orm import Mapped, mapped_column + +from app.models.base import Base + + +class FavoriteSite(Base): + __tablename__ = "favorite_sites" + + device_id: Mapped[str] = mapped_column(String(64), primary_key=True) + origin: Mapped[str] = mapped_column(String(64), primary_key=True) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + )