Limit bulk refresh to watching and planned
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1622,8 +1622,20 @@ 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:
|
||||||
|
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()
|
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()]
|
||||||
|
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user