Limit bulk refresh to watching and planned

This commit is contained in:
Dymas
2026-05-25 17:40:13 +02:00
parent aef3be9d2d
commit d10ae832b3
7 changed files with 80 additions and 20 deletions
+4
View File
@@ -1,5 +1,9 @@
# Changelog # Changelog
## 0.44.1 - 2026-05-25
- Limited bulk watchlist refreshes to entries marked `Watching` or `Planned`, so manual `Refresh All` runs and scheduled refreshes skip `Finished` and `Dropped` shows.
## 0.44.0 - 2026-05-25 ## 0.44.0 - 2026-05-25
- Added host filesystem browse buttons to the Config page path fields so default download and Jellyfin library folders can be selected from a built-in browser modal instead of typed manually. - Added host filesystem browse buttons to the Config page path fields so default download and Jellyfin library folders can be selected from a built-in browser modal instead of typed manually.
+1 -1
View File
@@ -2,7 +2,7 @@
Small local web UI for a system-wide `ani-cli` install. Small local web UI for a system-wide `ani-cli` install.
Current version: `0.44.0` Current version: `0.44.1`
## Project Layout ## Project Layout
+1 -1
View File
@@ -1 +1 @@
0.44.0 0.44.1
+15 -3
View File
@@ -1622,9 +1622,21 @@ class WatchlistStore:
raise KeyError("Anime not found in watchlist.") raise KeyError("Anime not found in watchlist.")
return self._row_to_item(row) return self._row_to_item(row)
def all_show_ids(self): def all_show_ids(self, categories=None):
normalized_categories = []
for category in categories or []:
normalized = normalize_watchlist_category(category)
if normalized not in normalized_categories:
normalized_categories.append(normalized)
with self.lock, self._connect() as conn: with self.lock, self._connect() as conn:
rows = conn.execute("SELECT show_id FROM watchlist ORDER BY title COLLATE NOCASE ASC").fetchall() if normalized_categories:
placeholders = ", ".join("?" for _ in normalized_categories)
rows = conn.execute(
f"SELECT show_id FROM watchlist WHERE category IN ({placeholders}) ORDER BY title COLLATE NOCASE ASC",
tuple(normalized_categories),
).fetchall()
else:
rows = conn.execute("SELECT show_id FROM watchlist ORDER BY title COLLATE NOCASE ASC").fetchall()
return [str(row["show_id"]).strip() for row in rows if str(row["show_id"]).strip()] return [str(row["show_id"]).strip() for row in rows if str(row["show_id"]).strip()]
def add(self, payload): def add(self, payload):
@@ -1840,7 +1852,7 @@ class WatchlistStore:
def refresh_all(self): def refresh_all(self):
refreshed = [] refreshed = []
for show_id in self.all_show_ids(): for show_id in self.all_show_ids(categories=("watching", "planned")):
item = self.refresh(show_id, include_thumbnail=False) item = self.refresh(show_id, include_thumbnail=False)
if item is not None: if item is not None:
refreshed.append(item) refreshed.append(item)
+1 -1
View File
@@ -20,7 +20,7 @@ from uuid import uuid4
PROJECT_ROOT = Path(__file__).resolve().parent PROJECT_ROOT = Path(__file__).resolve().parent
ANI_CLI = os.environ.get("ANI_CLI_BIN") or shutil.which("ani-cli") or "ani-cli" ANI_CLI = os.environ.get("ANI_CLI_BIN") or shutil.which("ani-cli") or "ani-cli"
APP_NAME = "ani-cli-web" APP_NAME = "ani-cli-web"
VERSION = "0.44.0" VERSION = "0.44.1"
ALLANIME_BASE = "allanime.day" ALLANIME_BASE = "allanime.day"
ALLANIME_API = f"https://api.{ALLANIME_BASE}" ALLANIME_API = f"https://api.{ALLANIME_BASE}"
ALLANIME_REFERER = "https://allmanga.to" ALLANIME_REFERER = "https://allmanga.to"
+1 -1
View File
@@ -268,7 +268,7 @@ class WatchlistRefreshJobs:
new_episode_items = [] new_episode_items = []
refresh_error_items = [] refresh_error_items = []
try: try:
show_ids = self.store.all_show_ids() show_ids = self.store.all_show_ids(categories=("watching", "planned"))
delay_seconds = self._refresh_delay_seconds() delay_seconds = self._refresh_delay_seconds()
started_at = now_iso() started_at = now_iso()
with self.lock: with self.lock:
+57 -13
View File
@@ -498,17 +498,61 @@ class DownloadQueueWorkerFailureTests(unittest.TestCase):
class WatchlistRefreshAllTests(unittest.TestCase): class WatchlistRefreshAllTests(unittest.TestCase):
def test_refresh_all_iterates_every_show_id(self): def test_refresh_all_iterates_only_watching_and_planned_show_ids(self):
store = object.__new__(APP.WatchlistStore) store = object.__new__(APP.WatchlistStore)
calls = [] calls = []
store.all_show_ids = lambda: ["show-a", "show-b", "show-c"] store.all_show_ids = lambda categories=None: ["show-a", "show-b"]
store.refresh = lambda show_id, include_thumbnail=True: calls.append((show_id, include_thumbnail)) or {"show_id": show_id} store.refresh = lambda show_id, include_thumbnail=True: calls.append((show_id, include_thumbnail)) or {"show_id": show_id}
result = APP.WatchlistStore.refresh_all(store) result = APP.WatchlistStore.refresh_all(store)
self.assertEqual(calls, [("show-a", False), ("show-b", False), ("show-c", False)]) self.assertEqual(calls, [("show-a", False), ("show-b", False)])
self.assertEqual(result["count"], 3) self.assertEqual(result["count"], 2)
self.assertEqual([item["show_id"] for item in result["items"]], ["show-a", "show-b", "show-c"]) self.assertEqual([item["show_id"] for item in result["items"]], ["show-a", "show-b"])
def test_all_show_ids_can_filter_by_category(self):
with APP.WATCHLIST._connect() as conn:
conn.execute("DELETE FROM watchlist")
now = APP.now_iso()
items = [
{"show_id": "show-w", "title": "Watching", "category": "watching"},
{"show_id": "show-p", "title": "Planned", "category": "planned"},
{"show_id": "show-f", "title": "Finished", "category": "finished"},
{"show_id": "show-d", "title": "Dropped", "category": "dropped"},
]
with APP.WATCHLIST.lock, APP.WATCHLIST._connect() as conn:
for item in items:
APP.WATCHLIST._upsert_conn(
conn,
{
**APP.WATCHLIST._auto_download_defaults(item["title"]),
"show_id": item["show_id"],
"title": item["title"],
"category": item["category"],
"downloaded": False,
"status": "tracked",
"status_message": "seeded",
"created_at": now,
"updated_at": now,
"last_checked": None,
"sub_count": 0,
"dub_count": 0,
"expected_count": 0,
"airing_status": None,
"sub_latest_episode": None,
"dub_latest_episode": None,
"animeschedule_route": None,
"animeschedule_title": None,
"anidb_aid": None,
"anidb_title": None,
"thumbnail_path": None,
"thumbnail_checked_at": None,
},
)
filtered = APP.WATCHLIST.all_show_ids(categories=("watching", "planned"))
self.assertEqual(filtered, ["show-p", "show-w"])
class WatchlistRefreshJobTests(unittest.TestCase): class WatchlistRefreshJobTests(unittest.TestCase):
@@ -518,7 +562,7 @@ class WatchlistRefreshJobTests(unittest.TestCase):
def test_refresh_job_tracks_completion_and_errors(self): def test_refresh_job_tracks_completion_and_errors(self):
class FakeStore: class FakeStore:
def all_show_ids(self): def all_show_ids(self, categories=None):
return ["show-a", "show-b"] return ["show-a", "show-b"]
def refresh(self, show_id, include_thumbnail=True): def refresh(self, show_id, include_thumbnail=True):
@@ -546,7 +590,7 @@ class WatchlistRefreshJobTests(unittest.TestCase):
calls = [] calls = []
class FakeStore: class FakeStore:
def all_show_ids(self): def all_show_ids(self, categories=None):
return ["show-a", "show-b"] return ["show-a", "show-b"]
def refresh(self, show_id, include_thumbnail=True): def refresh(self, show_id, include_thumbnail=True):
@@ -581,7 +625,7 @@ class WatchlistRefreshJobTests(unittest.TestCase):
"current_title": None, "current_title": None,
"message": "", "message": "",
} }
service.store.all_show_ids = lambda: ["show-a"] service.store.all_show_ids = lambda categories=None: ["show-a"]
service.store.refresh = mock.Mock(return_value={"title": "Show A", "status": "updated"}) service.store.refresh = mock.Mock(return_value={"title": "Show A", "status": "updated"})
service.stop_event.set() service.stop_event.set()
@@ -592,7 +636,7 @@ class WatchlistRefreshJobTests(unittest.TestCase):
def test_refresh_start_reuses_pending_job(self): def test_refresh_start_reuses_pending_job(self):
class FakeStore: class FakeStore:
def all_show_ids(self): def all_show_ids(self, categories=None):
return [] return []
def refresh(self, _show_id, include_thumbnail=True): def refresh(self, _show_id, include_thumbnail=True):
@@ -609,7 +653,7 @@ class WatchlistRefreshJobTests(unittest.TestCase):
def test_scheduled_refresh_logs_queue_and_completion(self): def test_scheduled_refresh_logs_queue_and_completion(self):
class FakeStore: class FakeStore:
def all_show_ids(self): def all_show_ids(self, categories=None):
return ["show-a"] return ["show-a"]
def refresh(self, _show_id, include_thumbnail=True): def refresh(self, _show_id, include_thumbnail=True):
@@ -629,7 +673,7 @@ class WatchlistRefreshJobTests(unittest.TestCase):
def test_refresh_history_marks_pending_jobs_interrupted_after_restart(self): def test_refresh_history_marks_pending_jobs_interrupted_after_restart(self):
class FakeStore: class FakeStore:
def all_show_ids(self): def all_show_ids(self, categories=None):
return [] return []
def refresh(self, _show_id, include_thumbnail=True): def refresh(self, _show_id, include_thumbnail=True):
@@ -1526,7 +1570,7 @@ class WatchlistAutoRefreshSchedulerTests(unittest.TestCase):
def test_refresh_job_waits_between_series_using_configured_delay(self): def test_refresh_job_waits_between_series_using_configured_delay(self):
class FakeStore: class FakeStore:
def all_show_ids(self): def all_show_ids(self, categories=None):
return ["show-a", "show-b", "show-c"] return ["show-a", "show-b", "show-c"]
def refresh(self, show_id, include_thumbnail=True): def refresh(self, show_id, include_thumbnail=True):
@@ -1548,7 +1592,7 @@ class WatchlistAutoRefreshSchedulerTests(unittest.TestCase):
def test_refresh_job_sends_new_episode_and_failure_notifications(self): def test_refresh_job_sends_new_episode_and_failure_notifications(self):
class FakeStore: class FakeStore:
def all_show_ids(self): def all_show_ids(self, categories=None):
return ["show-a", "show-b"] return ["show-a", "show-b"]
def refresh(self, show_id, include_thumbnail=True): def refresh(self, show_id, include_thumbnail=True):