refactor(api): shared serializer + ownership + write-scope helpers (R6 S1a)
Establish the R6 API-contract infra before the S1b payload:dict migration: - iso(dt) in utils.py replaces the ~16 hand-rolled `x.isoformat() if x else None` serializer idioms across the routes. - get_or_404 / owned_or_404 in routes/_common.py — one 404 + existence-leak policy; plex's _own_playlist_or_404 and saved_views' _own_view now delegate to it. - require_write_scope dependency in auth.py collapses the inline has_write_scope→403 checks (channels subscribe/unsubscribe, playlists push/push-plan) that had drifted into three different messages into one; the conditional delete_playlist guard stays inline (its message was already canonical). No response-shape change (iso == isoformat for a present value). Gate: ruff clean, pytest 163 green (+ test_utils for iso).
This commit is contained in:
@@ -9,8 +9,9 @@ from sqlalchemy import func, select, update
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import quota, sysconfig
|
||||
from app.auth import current_user, has_write_scope, require_human
|
||||
from app.auth import current_user, require_human, require_write_scope
|
||||
from app.db import get_db
|
||||
from app.utils import iso
|
||||
from app.models import (
|
||||
LIVE_OR_UPCOMING,
|
||||
BlockedChannel,
|
||||
@@ -67,7 +68,7 @@ def list_channels(
|
||||
).all():
|
||||
agg[cid] = {
|
||||
"stored": n,
|
||||
"last_video_at": last_up.isoformat() if last_up else None,
|
||||
"last_video_at": iso(last_up),
|
||||
"total_duration_seconds": int(total_dur or 0),
|
||||
"count_short": int(shorts),
|
||||
"count_live": int(live),
|
||||
@@ -232,7 +233,7 @@ def _channel_detail_dict(
|
||||
"subscriber_count": channel.subscriber_count,
|
||||
"video_count": channel.video_count,
|
||||
"total_view_count": channel.total_view_count,
|
||||
"published_at": channel.published_at.isoformat() if channel.published_at else None,
|
||||
"published_at": iso(channel.published_at),
|
||||
"external_links": channel.external_links or [],
|
||||
"country": channel.country,
|
||||
"default_language": channel.default_language,
|
||||
@@ -454,19 +455,14 @@ def reset_backfill(
|
||||
@router.post("/{channel_id}/subscribe")
|
||||
def subscribe(
|
||||
channel_id: str,
|
||||
user: User = Depends(current_user),
|
||||
user: User = Depends(require_write_scope),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""Subscribe to a channel discovered from the user's playlists. Gated behind the
|
||||
optional write scope (YouTube subscriptions.insert). Creates the local subscription
|
||||
with YouTube's returned resource id and enriches the channel's metadata if it's still
|
||||
a stub, so the new row renders properly in the manager. It does NOT pull the channel's
|
||||
videos — the scheduler picks those up on its next run, like any subscribed channel."""
|
||||
if not has_write_scope(user):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Enable playlist editing in Settings to subscribe on YouTube.",
|
||||
)
|
||||
optional write scope (YouTube subscriptions.insert) via require_write_scope. Creates the
|
||||
local subscription with YouTube's returned resource id and enriches the channel's metadata
|
||||
if it's still a stub, so the new row renders properly in the manager. It does NOT pull the
|
||||
channel's videos — the scheduler picks those up on its next run, like any subscribed channel."""
|
||||
channel = db.get(Channel, channel_id)
|
||||
if channel is None:
|
||||
raise HTTPException(status_code=404, detail="Unknown channel")
|
||||
@@ -524,16 +520,12 @@ def subscribe(
|
||||
@router.delete("/{channel_id}/subscription")
|
||||
def unsubscribe(
|
||||
channel_id: str,
|
||||
user: User = Depends(current_user),
|
||||
user: User = Depends(require_write_scope),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""Unsubscribe from this channel on YouTube and drop the local subscription. Gated
|
||||
behind the optional write scope; read-only users get a 403 and should Hide instead."""
|
||||
if not has_write_scope(user):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Enable playlist editing in Settings to unsubscribe on YouTube.",
|
||||
)
|
||||
behind the optional write scope (require_write_scope); read-only users get a 403 and
|
||||
should Hide instead."""
|
||||
sub = _user_subscription(db, user, channel_id)
|
||||
if not sub.yt_subscription_id:
|
||||
raise HTTPException(
|
||||
|
||||
Reference in New Issue
Block a user