fix(r5): address the /code-review high round — 10 findings
- HLS sweep only deletes directories matching the session-name shape; the root is admin-configurable and may be a shared path - playlist unwrap picks the first entry that actually downloaded, not entries[0] - concat quoting keeps backslashes literal (ffmpeg treats them so inside '') - ArrowUp/Down defer while the focused element can still scroll that way - the staging fallback skips yt-dlp working files (.part-Frag/.ytdl/.temp/.fNNN) - the boot HLS sweep runs off the event loop - volume persistence is debounced (was a setItem per wheel notch) and a spin no longer loses notches to the iframe's lagging getVolume - _Breaker.run owns the whole open/call/record protocol; four call sites shrink - should_prune extracted and unit-tested, plus RSS error-vs-empty tests - the worker exits non-zero when the schema never lands
This commit is contained in:
@@ -7,6 +7,7 @@ never synced. Videos in a playlist that aren't in our shared catalog yet are fet
|
||||
ingested (with a stub channel) so the mirror is faithful."""
|
||||
import hashlib
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import delete, select
|
||||
@@ -271,8 +272,16 @@ def plan_push(db: Session, user: User, pl: Playlist, *, live=None) -> dict:
|
||||
_MAX_CONSECUTIVE_FAILURES = 5
|
||||
|
||||
|
||||
#: Returned by `_Breaker.run` when the write did not happen (refused or failed).
|
||||
FAILED = object()
|
||||
|
||||
|
||||
class _Breaker:
|
||||
"""Consecutive-failure circuit breaker for a single push run. Any success re-arms it."""
|
||||
"""Consecutive-failure circuit breaker for a single push run. Any success re-arms it.
|
||||
|
||||
Drive it with `run()` — it owns the whole open-check / call / record-outcome sequence, so a
|
||||
write site can't accidentally implement two thirds of the protocol (forgetting to record a
|
||||
failure would silently disable the breaker for that operation)."""
|
||||
|
||||
def __init__(self, limit: int = _MAX_CONSECUTIVE_FAILURES) -> None:
|
||||
self.limit = limit
|
||||
@@ -296,6 +305,22 @@ class _Breaker:
|
||||
self.limit,
|
||||
)
|
||||
|
||||
def run(self, op: Callable[[], object], on_fail: Callable[[], None]) -> object:
|
||||
"""Attempt one YouTube write. Returns the op's result, or `FAILED` when the breaker is
|
||||
already tripped (op not called) or the op raised YouTubeError. `on_fail` records the item
|
||||
in the caller's failure list either way."""
|
||||
if self.tripped:
|
||||
on_fail()
|
||||
return FAILED
|
||||
try:
|
||||
out = op()
|
||||
except YouTubeError:
|
||||
on_fail()
|
||||
self.hit(False)
|
||||
return FAILED
|
||||
self.hit(True)
|
||||
return out
|
||||
|
||||
|
||||
def push_playlist(db: Session, user: User, pl: Playlist, *, live=None) -> dict:
|
||||
"""Push a local playlist to YouTube: create it if needed, then reconcile membership
|
||||
@@ -313,16 +338,11 @@ def push_playlist(db: Session, user: User, pl: Playlist, *, live=None) -> dict:
|
||||
created = 1
|
||||
# Brand-new playlist: just append every video in order.
|
||||
for vid in desired:
|
||||
if not breaker.open():
|
||||
failures.append(vid)
|
||||
continue
|
||||
try:
|
||||
yt.add_playlist_item(pl.yt_playlist_id, vid)
|
||||
if breaker.run(
|
||||
lambda v=vid: yt.add_playlist_item(pl.yt_playlist_id, v),
|
||||
lambda v=vid: failures.append(v),
|
||||
) is not FAILED:
|
||||
inserted += 1
|
||||
breaker.hit(True)
|
||||
except YouTubeError:
|
||||
failures.append(vid)
|
||||
breaker.hit(False)
|
||||
if not failures:
|
||||
pl.synced_fingerprint = fingerprint(pl.name, desired)
|
||||
pl.dirty = False
|
||||
@@ -354,30 +374,22 @@ def push_playlist(db: Session, user: User, pl: Playlist, *, live=None) -> dict:
|
||||
# Delete extras (local is authoritative).
|
||||
for it in current:
|
||||
if it["video_id"] not in desired_set:
|
||||
if not breaker.open():
|
||||
failures.append(it["video_id"])
|
||||
continue
|
||||
try:
|
||||
yt.delete_playlist_item(it["item_id"])
|
||||
if breaker.run(
|
||||
lambda i=it: yt.delete_playlist_item(i["item_id"]),
|
||||
lambda i=it: failures.append(i["video_id"]),
|
||||
) is not FAILED:
|
||||
deleted += 1
|
||||
breaker.hit(True)
|
||||
except YouTubeError:
|
||||
failures.append(it["video_id"])
|
||||
breaker.hit(False)
|
||||
# Insert missing (append; the reorder pass below fixes positions).
|
||||
cur_set = set(cur_vids)
|
||||
for vid in desired:
|
||||
if vid not in cur_set:
|
||||
if not breaker.open():
|
||||
failures.append(vid)
|
||||
continue
|
||||
try:
|
||||
item_id_by_vid[vid] = yt.add_playlist_item(pl.yt_playlist_id, vid)
|
||||
item_id = breaker.run(
|
||||
lambda v=vid: yt.add_playlist_item(pl.yt_playlist_id, v),
|
||||
lambda v=vid: failures.append(v),
|
||||
)
|
||||
if item_id is not FAILED:
|
||||
item_id_by_vid[vid] = item_id
|
||||
inserted += 1
|
||||
breaker.hit(True)
|
||||
except YouTubeError:
|
||||
failures.append(vid)
|
||||
breaker.hit(False)
|
||||
# Reorder to match `desired` via insertion-sort moves over a local model.
|
||||
model = [v for v in cur_vids if v in desired_set] + [
|
||||
v for v in desired if v not in cur_set and v in item_id_by_vid
|
||||
@@ -387,17 +399,14 @@ def push_playlist(db: Session, user: User, pl: Playlist, *, live=None) -> dict:
|
||||
continue # failed insert; skip
|
||||
if i < len(model) and model[i] == want:
|
||||
continue
|
||||
if not breaker.open():
|
||||
failures.append(want)
|
||||
continue
|
||||
try:
|
||||
yt.move_playlist_item(item_id_by_vid[want], pl.yt_playlist_id, want, i)
|
||||
reordered += 1
|
||||
breaker.hit(True)
|
||||
except YouTubeError:
|
||||
failures.append(want)
|
||||
breaker.hit(False)
|
||||
if breaker.run(
|
||||
lambda w=want, pos=i: yt.move_playlist_item(
|
||||
item_id_by_vid[w], pl.yt_playlist_id, w, pos
|
||||
),
|
||||
lambda w=want: failures.append(w),
|
||||
) is FAILED:
|
||||
continue
|
||||
reordered += 1
|
||||
j = model.index(want)
|
||||
model.pop(j)
|
||||
model.insert(i, want)
|
||||
|
||||
@@ -73,6 +73,18 @@ def apply_channel_details(db: Session, items: list[dict]) -> None:
|
||||
channel.details_synced_at = now
|
||||
|
||||
|
||||
def should_prune(fetched_count: int, existing_count: int) -> bool:
|
||||
"""Whether an import may delete the local subscriptions missing from this fetch.
|
||||
|
||||
NEVER on an empty fetch that contradicts local state: the API can answer 200 with no items (a
|
||||
transient blip, a scope/token edge), and pruning against that wipes every subscription the user
|
||||
has — taking each row's `deep_requested` flag with it — in a single run. Someone who genuinely
|
||||
unsubscribed from everything simply keeps their rows until a sync that returns something.
|
||||
|
||||
Pure so it can be tested without a database; `import_subscriptions` is the only caller."""
|
||||
return fetched_count > 0 or existing_count == 0
|
||||
|
||||
|
||||
def import_subscriptions(db: Session, user: User) -> dict:
|
||||
"""Pull all subscriptions for `user`, upsert channels + subscription links, prune
|
||||
channels the user has unsubscribed from on YouTube, and fetch channel details for
|
||||
@@ -108,18 +120,15 @@ def import_subscriptions(db: Session, user: User) -> dict:
|
||||
sub_row.yt_subscription_id = sub.get("yt_subscription_id")
|
||||
db.flush()
|
||||
|
||||
# Prune subscriptions removed on YouTube — but NEVER on an empty fetch. The API can answer
|
||||
# 200 with no items (a transient blip, a scope/token edge), and pruning against that wipes
|
||||
# every subscription the user has, taking each row's `deep_requested` flag with it, in one
|
||||
# run. Someone who genuinely unsubscribed from everything simply keeps their rows until a
|
||||
# sync that returns something.
|
||||
# Prune subscriptions removed on YouTube — but only when this fetch is trustworthy
|
||||
# (see `should_prune`).
|
||||
removed = 0
|
||||
existing = (
|
||||
db.execute(select(Subscription).where(Subscription.user_id == user.id))
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
prune_skipped = bool(existing) and not fetched
|
||||
prune_skipped = not should_prune(len(fetched), len(existing))
|
||||
if prune_skipped:
|
||||
log.warning(
|
||||
"Subscription import (user %s): YouTube returned 0 subscriptions while %d exist "
|
||||
|
||||
Reference in New Issue
Block a user