diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78b56fd..3469639 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## 0.45.18 - 2026-06-13
+
+- Added a `Source name` field to the watchlist auto-download editor so per-show jobs can send a more specific anime title to `ani-cli` when provider searches return multiple similarly named results.
+- Migrated existing watchlist databases by adding an `auto_download_source_name` column and defaulting stored entries to the tracked title, preserving current behavior until you override it.
+- Changed both watchlist `Download all` and automatic watchlist episode queueing to use the saved `Source name` for the `ani-cli` query while keeping the separate library `Name` field for folder and file naming.
+
## 0.45.17 - 2026-06-07
- Added a lightweight `/api/watchlist/homepage` endpoint for Homepage's Custom API widget, returning simple watchlist category counts for `watchlist`, `planned`, `finished`, `dropped`, and `total`.
diff --git a/README.md b/README.md
index ad0462f..fa809d6 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Local web UI for a system-wide `ani-cli` install.
-Current version: `0.45.17`
+Current version: `0.45.18`
## What it does
@@ -11,7 +11,7 @@ Current version: `0.45.17`
- Track shows in a watchlist with `Watching`, `Planned`, `Finished`, and `Dropped` categories
- Expose a small JSON summary endpoint for Homepage dashboard widgets
- Refresh watchlist episode counts manually or on a schedule
-- Auto-download missing episodes for `Watching` entries after later refreshes
+- Auto-download missing episodes for `Watching` entries after later refreshes, with a per-show `Source name` override for `ani-cli` search
- Move fully completed libraries into Jellyfin TV or movie folders
- Send optional Discord webhook notifications
@@ -19,7 +19,7 @@ Current version: `0.45.17`
- `/`: Search and queue downloads, with poster-card search results below the selection box and alternate English titles under the main name when available
- `/queue`: Monitor jobs, inspect logs, retry failed jobs, remove finished jobs
-- `/watchlist`: Track shows, refresh them, download all available episodes, manage auto-download settings
+- `/watchlist`: Track shows, refresh them, download all available episodes, and manage auto-download settings such as `Source name`, library `Name`, season, and episode offset
- `/config`: Save defaults, schedule refreshes, configure Jellyfin and Discord, view runtime info and the changelog
## Quick start
@@ -96,7 +96,7 @@ Docker notes:
### Auto-download
- Runs only for entries in `Watching`
-- Uses the per-show language, quality, season, library name, and optional episode offset settings
+- Uses the per-show language, quality, `Source name` search override, season, library name, and optional episode offset settings
- Can queue backlog episodes that were already available before the show was first tracked
### Jellyfin handoff
diff --git a/VERSION b/VERSION
index ae90b2b..349248c 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.45.17
+0.45.18
diff --git a/app.py b/app.py
index 1226907..9e2d5f5 100644
--- a/app.py
+++ b/app.py
@@ -1338,10 +1338,12 @@ class WatchlistStore:
def _auto_download_defaults(self, title):
config = self.config_getter() or {}
+ normalized_title = str(title or "").strip() or "Anime"
return {
"auto_download_mode": str(config.get("auto_download_mode") or "dub").strip().lower() or "dub",
"auto_download_quality": str(config.get("auto_download_quality") or "best").strip().lower() or "best",
- "auto_download_name": sanitize_path_component(title, "Anime"),
+ "auto_download_name": sanitize_path_component(normalized_title, "Anime"),
+ "auto_download_source_name": normalized_title,
"auto_download_series": "1",
"auto_download_offset": None,
"media_type": "tv",
@@ -1422,6 +1424,7 @@ class WatchlistStore:
auto_download_mode TEXT,
auto_download_quality TEXT,
auto_download_name TEXT,
+ auto_download_source_name TEXT,
auto_download_series TEXT,
auto_download_offset TEXT,
downloaded_sub_episodes_json TEXT,
@@ -1468,6 +1471,8 @@ class WatchlistStore:
conn.execute("ALTER TABLE watchlist ADD COLUMN auto_download_quality TEXT")
if "auto_download_name" not in columns:
conn.execute("ALTER TABLE watchlist ADD COLUMN auto_download_name TEXT")
+ if "auto_download_source_name" not in columns:
+ conn.execute("ALTER TABLE watchlist ADD COLUMN auto_download_source_name TEXT")
if "auto_download_series" not in columns:
conn.execute("ALTER TABLE watchlist ADD COLUMN auto_download_series TEXT")
if "auto_download_offset" not in columns:
@@ -1548,6 +1553,7 @@ class WatchlistStore:
item["auto_download_mode"] = mode if mode in MODE_CHOICES else defaults["auto_download_mode"]
item["auto_download_quality"] = quality if quality in QUALITY_CHOICES else defaults["auto_download_quality"]
item["auto_download_name"] = sanitize_path_component(item.get("auto_download_name") or defaults["auto_download_name"], defaults["auto_download_name"])
+ item["auto_download_source_name"] = str(item.get("auto_download_source_name") or defaults["auto_download_source_name"]).strip() or defaults["auto_download_source_name"]
item["auto_download_series"] = normalize_season(item.get("auto_download_series") or defaults["auto_download_series"])
item["auto_download_offset"] = normalize_episode_offset(item.get("auto_download_offset"))
item["downloaded_sub_episodes"] = decode_episode_values(item.get("downloaded_sub_episodes_json"))
@@ -1574,10 +1580,10 @@ class WatchlistStore:
animeschedule_route, animeschedule_title,
anilist_id, anilist_title, anilist_site_url, alternative_titles_json,
anidb_aid, anidb_title, media_type,
- auto_download_mode, auto_download_quality, auto_download_name, auto_download_series, auto_download_offset,
+ auto_download_mode, auto_download_quality, auto_download_name, auto_download_source_name, auto_download_series, auto_download_offset,
downloaded_sub_episodes_json, downloaded_dub_episodes_json,
thumbnail_path, thumbnail_checked_at
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(show_id) DO UPDATE SET
title = excluded.title,
category = excluded.category,
@@ -1604,6 +1610,7 @@ class WatchlistStore:
auto_download_mode = excluded.auto_download_mode,
auto_download_quality = excluded.auto_download_quality,
auto_download_name = excluded.auto_download_name,
+ auto_download_source_name = excluded.auto_download_source_name,
auto_download_series = excluded.auto_download_series,
auto_download_offset = excluded.auto_download_offset,
downloaded_sub_episodes_json = excluded.downloaded_sub_episodes_json,
@@ -1639,6 +1646,7 @@ class WatchlistStore:
item.get("auto_download_mode"),
item.get("auto_download_quality"),
item.get("auto_download_name"),
+ str(item.get("auto_download_source_name") or "").strip() or str(item["title"]).strip(),
item.get("auto_download_series"),
normalize_episode_offset(item.get("auto_download_offset")),
item.get("downloaded_sub_episodes_json"),
@@ -1696,6 +1704,7 @@ class WatchlistStore:
"auto_download_mode": None,
"auto_download_quality": None,
"auto_download_name": None,
+ "auto_download_source_name": None,
"auto_download_series": None,
"auto_download_offset": None,
"downloaded_sub_episodes_json": None,
@@ -1867,6 +1876,9 @@ class WatchlistStore:
}
item["auto_download_series"] = normalize_season(payload.get("auto_download_series") or item.get("auto_download_series") or "1")
item["auto_download_offset"] = normalize_episode_offset(payload.get("auto_download_offset"))
+ item["auto_download_source_name"] = (
+ str(payload.get("auto_download_source_name") or item.get("auto_download_source_name") or title).strip() or title
+ )
self._upsert_conn(conn, item)
self._clear_removal_conn(conn, show_id)
@@ -2196,6 +2208,7 @@ class WatchlistStore:
"anidb_aid": None,
"anidb_title": None,
"media_type": normalized_media_type,
+ "auto_download_source_name": None,
"auto_download_series": None,
"downloaded_sub_episodes_json": encode_episode_values(downloaded_episode_values) if normalized_mode == "sub" else None,
"downloaded_dub_episodes_json": encode_episode_values(downloaded_episode_values) if normalized_mode == "dub" else None,
@@ -2206,7 +2219,7 @@ class WatchlistStore:
self.schedule_refresh(normalized_show_id, source="sync")
return self.get(normalized_show_id)
- def update_auto_download_settings(self, show_id, mode=None, quality=None, name=None, series=None, episode_offset=None):
+ def update_auto_download_settings(self, show_id, mode=None, quality=None, name=None, source_name=None, series=None, episode_offset=None):
existing = self.get(show_id)
defaults = self._auto_download_defaults(existing["title"])
normalized_mode = str(mode or defaults["auto_download_mode"]).strip().lower()
@@ -2216,6 +2229,9 @@ class WatchlistStore:
if normalized_quality not in QUALITY_CHOICES:
raise ValueError("Unknown auto-download quality.")
normalized_name = sanitize_path_component(name or defaults["auto_download_name"], defaults["auto_download_name"])
+ normalized_source_name = str(source_name or existing.get("auto_download_source_name") or defaults["auto_download_source_name"]).strip()
+ if not normalized_source_name:
+ normalized_source_name = defaults["auto_download_source_name"]
normalized_series = normalize_season(series or existing.get("auto_download_series") or defaults["auto_download_series"])
normalized_offset = (
normalize_episode_offset(existing.get("auto_download_offset"))
@@ -2226,10 +2242,19 @@ class WatchlistStore:
conn.execute(
"""
UPDATE watchlist
- SET auto_download_mode = ?, auto_download_quality = ?, auto_download_name = ?, auto_download_series = ?, auto_download_offset = ?, updated_at = ?
+ SET auto_download_mode = ?, auto_download_quality = ?, auto_download_name = ?, auto_download_source_name = ?, auto_download_series = ?, auto_download_offset = ?, updated_at = ?
WHERE show_id = ?
""",
- (normalized_mode, normalized_quality, normalized_name, normalized_series, normalized_offset, now_iso(), existing["show_id"]),
+ (
+ normalized_mode,
+ normalized_quality,
+ normalized_name,
+ normalized_source_name,
+ normalized_series,
+ normalized_offset,
+ now_iso(),
+ existing["show_id"],
+ ),
)
return self.get(show_id)
@@ -2442,7 +2467,7 @@ def download_watchlist_item(show_id, mode):
if normalized_mode not in MODE_CHOICES:
raise ValueError("Mode must be sub or dub")
- query = str(item.get("title") or item.get("show_id") or "").strip()
+ query = str(item.get("auto_download_source_name") or item.get("title") or item.get("show_id") or "").strip()
if not query:
raise ValueError("Watchlist item is missing a searchable title.")
@@ -2502,7 +2527,7 @@ def queue_watchlist_auto_download(item, refresh_source="manual"):
if not specs:
return {"queued": False, "reason": "nothing_missing"}
- query = str(item.get("title") or item.get("show_id") or "").strip()
+ query = str(item.get("auto_download_source_name") or item.get("title") or item.get("show_id") or "").strip()
if not query:
return {"queued": False, "reason": "missing_title"}
@@ -2619,13 +2644,14 @@ def update_watchlist_media_type(show_id, media_type):
return {"message": f"Saved {item['title']} as {item['media_type_label']}.", "item": item}
-def update_watchlist_auto_download(show_id, mode=None, quality=None, name=None, series=None, episode_offset=None):
+def update_watchlist_auto_download(show_id, mode=None, quality=None, name=None, source_name=None, series=None, episode_offset=None):
ensure_runtime()
item = WATCHLIST.update_auto_download_settings(
show_id,
mode=mode,
quality=quality,
name=name,
+ source_name=source_name,
series=series,
episode_offset=episode_offset,
)
diff --git a/http_handler.py b/http_handler.py
index 2349544..509cd57 100644
--- a/http_handler.py
+++ b/http_handler.py
@@ -729,6 +729,7 @@ class Handler(BaseHTTPRequestHandler):
payload.get("mode"),
payload.get("quality"),
payload.get("name"),
+ payload.get("source_name"),
payload.get("series"),
payload.get("episode_offset"),
)
diff --git a/test_app.py b/test_app.py
index 46ed758..711ce0a 100644
--- a/test_app.py
+++ b/test_app.py
@@ -1598,6 +1598,7 @@ class WatchlistCompletionTests(unittest.TestCase):
"show_id": "show-42",
"title": "Queue Show",
"auto_download_name": "Queue Show Library",
+ "auto_download_source_name": "Queue Show Source",
"auto_download_series": "4",
"auto_download_quality": "720",
"media_type": "movie",
@@ -1613,7 +1614,7 @@ class WatchlistCompletionTests(unittest.TestCase):
runtime["download_queue"].add.assert_called_once_with(
{
"show_id": "show-42",
- "query": "Queue Show",
+ "query": "Queue Show Source",
"title": "Queue Show",
"anime_name": "Queue Show Library",
"media_type": "movie",
@@ -1656,6 +1657,7 @@ class WatchlistCompletionTests(unittest.TestCase):
"show_id": "show-42",
"title": "Queue Show",
"auto_download_name": "Queue Show",
+ "auto_download_source_name": "Queue Show Alt Search",
"auto_download_series": "1",
"media_type": "tv",
}
@@ -2339,6 +2341,7 @@ class AutoDownloadQueueTests(unittest.TestCase):
"auto_download_mode": "dub",
"auto_download_quality": "best",
"auto_download_name": 'Queue Show: Alt/Name',
+ "auto_download_source_name": "Queue Show Source",
"auto_download_series": "3",
"auto_download_offset": "13",
"downloaded_dub_episodes": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
@@ -2370,7 +2373,7 @@ class AutoDownloadQueueTests(unittest.TestCase):
download_queue.add.assert_called_once_with(
{
"show_id": "show-42",
- "query": "Queue Show",
+ "query": "Queue Show Source",
"title": "Queue Show",
"anime_name": "Queue Show- Alt-Name",
"media_type": "tv",
@@ -2395,6 +2398,7 @@ class AutoDownloadQueueTests(unittest.TestCase):
"auto_download_mode": "dub",
"auto_download_quality": "best",
"auto_download_name": "Backlog Show",
+ "auto_download_source_name": "Backlog Show Search",
"auto_download_series": "1",
"auto_download_offset": None,
"downloaded_dub_episodes": [],
@@ -2424,7 +2428,7 @@ class AutoDownloadQueueTests(unittest.TestCase):
download_queue.add.assert_called_once_with(
{
"show_id": "show-99",
- "query": "Backlog Show",
+ "query": "Backlog Show Search",
"title": "Backlog Show",
"anime_name": "Backlog Show",
"media_type": "tv",
@@ -2449,6 +2453,7 @@ class AutoDownloadQueueTests(unittest.TestCase):
"auto_download_mode": "dub",
"auto_download_quality": "best",
"auto_download_name": "First Refresh Show",
+ "auto_download_source_name": "First Refresh Source",
"auto_download_series": "1",
"auto_download_offset": None,
"downloaded_dub_episodes": [],
@@ -2478,7 +2483,7 @@ class AutoDownloadQueueTests(unittest.TestCase):
download_queue.add.assert_called_once_with(
{
"show_id": "show-first-refresh",
- "query": "First Refresh Show",
+ "query": "First Refresh Source",
"title": "First Refresh Show",
"anime_name": "First Refresh Show",
"media_type": "tv",
@@ -2503,6 +2508,7 @@ class AutoDownloadQueueTests(unittest.TestCase):
"auto_download_mode": "dub",
"auto_download_quality": "best",
"auto_download_name": "Split Season Show",
+ "auto_download_source_name": "Split Season Search",
"auto_download_series": "2",
"auto_download_offset": "13",
"downloaded_dub_episodes": [],
@@ -2531,7 +2537,7 @@ class AutoDownloadQueueTests(unittest.TestCase):
download_queue.add.assert_called_once_with(
{
"show_id": "show-77",
- "query": "Split Season Show",
+ "query": "Split Season Search",
"title": "Split Season Show",
"anime_name": "Split Season Show",
"media_type": "tv",
@@ -3089,7 +3095,7 @@ class HandlerRouteTests(unittest.TestCase):
self.assertEqual(handler.json_status, HTTPStatus.CREATED)
def test_watchlist_auto_download_settings_post_returns_updated_item(self):
- body = b'{"show_id":"show-1","mode":"dub","quality":"best","name":"Example Name","series":"2","episode_offset":"13"}'
+ body = b'{"show_id":"show-1","mode":"dub","quality":"best","name":"Example Name","source_name":"Example Source","series":"2","episode_offset":"13"}'
handler = DummyHandler("/api/watchlist/update-auto-download", body=body, content_length=len(body))
payload = {"message": "Saved auto-download settings for Show 1.", "item": {"show_id": "show-1"}}
handler.handler_context = mock.Mock(update_watchlist_auto_download=mock.Mock(return_value=payload))
@@ -3097,7 +3103,7 @@ class HandlerRouteTests(unittest.TestCase):
self.assertEqual(handler.json_payload, payload)
self.assertEqual(handler.json_status, HTTPStatus.OK)
handler.handler_context.update_watchlist_auto_download.assert_called_once_with(
- "show-1", "dub", "best", "Example Name", "2", "13"
+ "show-1", "dub", "best", "Example Name", "Example Source", "2", "13"
)
def test_watchlist_media_type_post_returns_updated_item(self):
@@ -3316,6 +3322,8 @@ class TemplateHelperTests(unittest.TestCase):
self.assertIn("closeAutoDownloadOverlay()", APP.WATCHLIST_HTML)
self.assertIn("Save auto-download", APP.WATCHLIST_HTML)
self.assertIn("Manual add does not trigger it", APP.WATCHLIST_HTML)
+ self.assertIn("Source name", APP.WATCHLIST_HTML)
+ self.assertIn('id="autoDownloadSourceName"', APP.WATCHLIST_HTML)
self.assertIn("Name source", APP.WATCHLIST_HTML)
self.assertIn('class="wide-half">Name source', APP.WATCHLIST_HTML)
self.assertIn('class="wide-half">Name', APP.WATCHLIST_HTML)
diff --git a/watchlist_page.py b/watchlist_page.py
index 6fbbae4..4ac491d 100644
--- a/watchlist_page.py
+++ b/watchlist_page.py
@@ -833,6 +833,9 @@ WATCHLIST_HTML = r"""
+
@@ -887,6 +890,7 @@ WATCHLIST_HTML = r"""
const autoDownloadQuality = document.getElementById("autoDownloadQuality");
const autoDownloadSeries = document.getElementById("autoDownloadSeries");
const autoDownloadOffset = document.getElementById("autoDownloadOffset");
+ const autoDownloadSourceName = document.getElementById("autoDownloadSourceName");
const autoDownloadNameSelect = document.getElementById("autoDownloadNameSelect");
const autoDownloadName = document.getElementById("autoDownloadName");
const autoDownloadSaveBtn = document.getElementById("autoDownloadSaveBtn");
@@ -1088,6 +1092,7 @@ WATCHLIST_HTML = r"""
autoDownloadQuality.value = item.auto_download_quality || "best";
autoDownloadSeries.value = item.auto_download_series || "1";
autoDownloadOffset.value = item.auto_download_offset || "";
+ autoDownloadSourceName.value = item.auto_download_source_name || item.title || "";
populateAutoDownloadNameChoices(item);
autoDownloadOverlay.hidden = false;
}
@@ -1123,6 +1128,7 @@ WATCHLIST_HTML = r"""
mode: autoDownloadMode.value,
quality: autoDownloadQuality.value,
name: autoDownloadName.value,
+ source_name: autoDownloadSourceName.value,
series: autoDownloadSeries.value,
episode_offset: autoDownloadOffset.value
})
@@ -1133,12 +1139,14 @@ WATCHLIST_HTML = r"""
auto_download_mode: savedItem.auto_download_mode || autoDownloadMode.value,
auto_download_quality: savedItem.auto_download_quality || autoDownloadQuality.value,
auto_download_name: savedItem.auto_download_name || autoDownloadName.value,
+ auto_download_source_name: savedItem.auto_download_source_name || autoDownloadSourceName.value,
auto_download_series: savedItem.auto_download_series || autoDownloadSeries.value,
auto_download_offset: savedItem.auto_download_offset || ""
};
populateAutoDownloadNameChoices(state.autoDownloadDraft);
autoDownloadSeries.value = state.autoDownloadDraft.auto_download_series || autoDownloadSeries.value;
autoDownloadOffset.value = state.autoDownloadDraft.auto_download_offset || "";
+ autoDownloadSourceName.value = state.autoDownloadDraft.auto_download_source_name || autoDownloadSourceName.value;
autoDownloadStatus.textContent = data.message || "Auto-download settings saved.";
setNotice(data.message || "Auto-download settings saved.");
closeAutoDownloadOverlay();