video: OMDb daily-limit cools down + auto-resumes (vs hard pause)

'Request limit reached!' is the free-tier daily quota (1,000/day), not a bad key
— so the worker now idles ~30 min and auto-resumes instead of pausing for good.
A library bigger than the daily cap just spreads its ratings across days on its
own. A genuinely invalid/unactivated key still hard-pauses until fixed. Cooldown
reads as paused in the UI (+ a 'cooldown' flag and note). Item is never burned to
synced, so nothing is lost.
This commit is contained in:
BoulderBadgeDad 2026-06-15 09:34:47 -07:00
parent 38d48bae37
commit c5ff2567a8
2 changed files with 48 additions and 7 deletions

View file

@ -10,6 +10,7 @@ video.db helpers; no music code.
from __future__ import annotations
import threading
import time
from utils.logging_config import get_logger
@ -38,6 +39,7 @@ class VideoEnrichmentWorker:
self.stats = {"matched": 0, "not_found": 0, "errors": 0}
self.note = None # a human reason when auto-paused (e.g. bad key)
self._rating_errors = 0 # consecutive ratings failures (transient backoff)
self._cooldown_until = 0.0 # monotonic time to idle until (daily-limit backoff)
# ── lifecycle ─────────────────────────────────────────────────────────────
def start(self):
@ -89,6 +91,12 @@ class VideoEnrichmentWorker:
if self.paused or not self.enabled:
self._stop.wait(1.0)
continue
# Daily-limit cooldown: idle until the quota resets, then auto-resume
# (re-checks periodically so we pick back up shortly after midnight UTC).
if self._cooldown_until > time.monotonic():
self.current_item = None
self._stop.wait(15.0)
continue
try:
did = self.process_one()
except Exception:
@ -160,12 +168,24 @@ class VideoEnrichmentWorker:
try:
r = self.client.ratings(item["imdb_id"])
except OMDbAuthError as e:
# Bad/expired key affects EVERY item — pause instead of churning the
# whole library + flooding the log. Transient pause (not persisted) so
# fixing the key (which rebuilds the engine) resumes automatically. The
# item is NOT marked synced, so it'll be tried again once the key works.
msg = str(e)
if "limit" in msg.lower():
# Free-tier daily quota (1,000/req) — resets at midnight UTC. Cool
# down and AUTO-RESUME (so a big library just spreads across days)
# rather than pausing for good. Item not marked synced → retried.
if self._cooldown_until <= time.monotonic():
logger.warning("OMDb daily request limit reached — idling ratings ~30 min; "
"auto-resumes after the daily reset")
self._cooldown_until = time.monotonic() + 1800
self.note = "OMDb daily limit reached — resumes after reset"
return False
# Bad/expired/unactivated key affects EVERY item — pause instead of
# churning the whole library + flooding the log. Transient pause (not
# persisted) so fixing the key (which rebuilds the engine) resumes
# automatically. The item is NOT marked synced, so it retries once
# the key works.
if not self.paused:
logger.warning("OMDb rejected the API key — pausing ratings until it's fixed (%s)", e)
logger.warning("OMDb rejected the API key — pausing ratings until it's fixed (%s)", msg)
self.note = "OMDb API key rejected"
self.pause(persist=False)
return False
@ -182,6 +202,7 @@ class VideoEnrichmentWorker:
self._rating_errors = 0
return False
self._rating_errors = 0
self._cooldown_until = 0.0
self.note = None
if r:
self.db.apply_ratings(item["kind"], item["id"], r) # marks synced
@ -258,7 +279,8 @@ class VideoEnrichmentWorker:
pending += self.db.episode_sync_pending_count()
except Exception:
pass
running = self.running and not self.paused and self.enabled
cooling = self._cooldown_until > time.monotonic()
running = self.running and not self.paused and self.enabled and not cooling
idle = running and pending == 0 and self.current_item is None
progress = {}
for kind, b in breakdown.items():
@ -269,10 +291,11 @@ class VideoEnrichmentWorker:
return {
"enabled": self.enabled,
"running": running,
"paused": self.paused,
"paused": self.paused or cooling, # cooldown reads as paused in the UI
"idle": idle,
"current_item": self.current_item,
"note": self.note,
"cooldown": cooling,
"stats": {**self.stats, "pending": pending},
"progress": progress,
"breakdown": breakdown,

View file

@ -341,6 +341,24 @@ def test_omdb_worker_pauses_on_bad_key_without_burning_items(db):
assert db.ratings_next() is not None # item NOT burned to 'synced'
def test_omdb_worker_cools_down_on_daily_limit_not_hard_pause(db):
# 'Request limit reached!' is the free-tier daily quota — cool down + auto
# resume (don't hard-pause), so a big library spreads across days.
from core.video.enrichment.clients import OMDbAuthError
db.upsert_movie("plex", {"server_id": "m1", "title": "A", "imdb_id": "tt1"})
class Omdb:
enabled = True
def ratings(self, imdb_id): raise OMDbAuthError("Request limit reached!")
w = VideoEnrichmentWorker(db, "omdb", Omdb())
assert w.process_one() is False
assert w.paused is False # NOT a hard pause (auto-resumes)
assert w._cooldown_until > 0 # cooling down instead
assert w.get_stats()["cooldown"] is True and w.get_stats()["paused"] is True
assert db.ratings_next() is not None # item not burned to synced
def test_omdb_worker_keeps_item_on_transient_error(db):
db.upsert_movie("plex", {"server_id": "m1", "title": "A", "imdb_id": "tt1"})