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:
2026-07-23 23:25:06 +02:00
parent 5130584d0a
commit 2a7f49c981
9 changed files with 307 additions and 79 deletions
+47 -38
View File
@@ -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)
+15 -6
View File
@@ -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 "