diff --git a/CHANGELOG.md b/CHANGELOG.md index 466076b..b2a8ce6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 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 - 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. diff --git a/README.md b/README.md index 394c438..7707a5b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Small local web UI for a system-wide `ani-cli` install. -Current version: `0.44.0` +Current version: `0.44.1` ## Project Layout diff --git a/VERSION b/VERSION index a8ab6c9..73f8a5a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.44.0 +0.44.1 diff --git a/app.py b/app.py index c36bb1c..1d092d1 100644 --- a/app.py +++ b/app.py @@ -1622,9 +1622,21 @@ class WatchlistStore: raise KeyError("Anime not found in watchlist.") 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: - 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()] def add(self, payload): @@ -1840,7 +1852,7 @@ class WatchlistStore: def refresh_all(self): 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) if item is not None: refreshed.append(item) diff --git a/app_support.py b/app_support.py index 75a130e..c12e6ce 100644 --- a/app_support.py +++ b/app_support.py @@ -20,7 +20,7 @@ from uuid import uuid4 PROJECT_ROOT = Path(__file__).resolve().parent ANI_CLI = os.environ.get("ANI_CLI_BIN") or shutil.which("ani-cli") or "ani-cli" APP_NAME = "ani-cli-web" -VERSION = "0.44.0" +VERSION = "0.44.1" ALLANIME_BASE = "allanime.day" ALLANIME_API = f"https://api.{ALLANIME_BASE}" ALLANIME_REFERER = "https://allmanga.to" diff --git a/queue_jobs.py b/queue_jobs.py index b78adc1..5206c1b 100644 --- a/queue_jobs.py +++ b/queue_jobs.py @@ -268,7 +268,7 @@ class WatchlistRefreshJobs: new_episode_items = [] refresh_error_items = [] try: - show_ids = self.store.all_show_ids() + show_ids = self.store.all_show_ids(categories=("watching", "planned")) delay_seconds = self._refresh_delay_seconds() started_at = now_iso() with self.lock: diff --git a/test_app.py b/test_app.py index 7ebcbfc..8b98a93 100644 --- a/test_app.py +++ b/test_app.py @@ -498,17 +498,61 @@ class DownloadQueueWorkerFailureTests(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) 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} result = APP.WatchlistStore.refresh_all(store) - self.assertEqual(calls, [("show-a", False), ("show-b", False), ("show-c", False)]) - self.assertEqual(result["count"], 3) - self.assertEqual([item["show_id"] for item in result["items"]], ["show-a", "show-b", "show-c"]) + self.assertEqual(calls, [("show-a", False), ("show-b", False)]) + self.assertEqual(result["count"], 2) + 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): @@ -518,7 +562,7 @@ class WatchlistRefreshJobTests(unittest.TestCase): def test_refresh_job_tracks_completion_and_errors(self): class FakeStore: - def all_show_ids(self): + def all_show_ids(self, categories=None): return ["show-a", "show-b"] def refresh(self, show_id, include_thumbnail=True): @@ -546,7 +590,7 @@ class WatchlistRefreshJobTests(unittest.TestCase): calls = [] class FakeStore: - def all_show_ids(self): + def all_show_ids(self, categories=None): return ["show-a", "show-b"] def refresh(self, show_id, include_thumbnail=True): @@ -581,7 +625,7 @@ class WatchlistRefreshJobTests(unittest.TestCase): "current_title": None, "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.stop_event.set() @@ -592,7 +636,7 @@ class WatchlistRefreshJobTests(unittest.TestCase): def test_refresh_start_reuses_pending_job(self): class FakeStore: - def all_show_ids(self): + def all_show_ids(self, categories=None): return [] 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): class FakeStore: - def all_show_ids(self): + def all_show_ids(self, categories=None): return ["show-a"] 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): class FakeStore: - def all_show_ids(self): + def all_show_ids(self, categories=None): return [] 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): class FakeStore: - def all_show_ids(self): + def all_show_ids(self, categories=None): return ["show-a", "show-b", "show-c"] 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): class FakeStore: - def all_show_ids(self): + def all_show_ids(self, categories=None): return ["show-a", "show-b"] def refresh(self, show_id, include_thumbnail=True):