diff --git a/CHANGELOG.md b/CHANGELOG.md index 80824c8..f894b17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.52.30 - 2026-09-09 + +- Allowed season/series `0` in search and watchlist auto-download settings so Jellyfin specials finalize under `Season 00` with `S00E..` filenames. + ## 0.52.29 - 2026-09-07 - Added retry and exponential backoff for temporary HLS segment network failures, including DNS resolution, connection, and socket timeout errors. diff --git a/README.md b/README.md index f36d9d1..b167a2a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Kaizoku is a local web app for searching, tracking, and downloading anime from A - Monitor active Queue jobs with per-episode progress, segment counts, and live downloader output. - Open result actions in a floating window for watchlist, media type, subbed/dubbed mode, library name, season, episode range, and download folder choices. - Fall back across the other configured providers when an episode or stream cannot be resolved on the primary provider. -- Save files with Jellyfin-friendly layout: `TV/Series Name/Season 01/Series Name - S01E01.mp4`. +- Save files with Jellyfin-friendly layout: `TV/Series Name/Season 01/Series Name - S01E01.mp4`, including Season 00 for specials. - Save English external subtitles when the provider exposes usable subtitle tracks. - Manage focused watchlists for `Watching`, `Planned`, `Finished`, and `Dropped`, with provider source tags and title links back to the original provider page. - Periodically refresh selected watchlists with a single visible advancing progress bar and auto-download newly available episodes. diff --git a/VERSION b/VERSION index 819b47c..f0b8ff5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.52.29 +0.52.30 diff --git a/app.py b/app.py index 7247718..5392902 100644 --- a/app.py +++ b/app.py @@ -1048,7 +1048,7 @@ def watchlist_source_season_dir(item, config): source_root = watchlist_source_library_dir(item, config) if normalize_media_type(item.get("media_type")) == "movie": return source_root - season = normalize_season((item or {}).get("auto_download_series") or "1") + season = normalize_season((item or {}).get("auto_download_series")) return source_root / f"Season {int(season):02d}" @@ -1145,7 +1145,7 @@ def watchlist_downloaded_episode_values_from_filesystem(item, config, mode): season_dir = watchlist_source_season_dir(item, config) if not season_dir.exists() or not season_dir.is_dir(): return [] - season = normalize_season((item or {}).get("auto_download_series") or "1") + season = normalize_season((item or {}).get("auto_download_series")) pattern = re.compile(rf"\bS{int(season):02d}E([0-9][0-9A-Za-z.\-]*)\b", re.IGNORECASE) values = [] seen = set() @@ -1703,7 +1703,7 @@ class WatchlistStore: 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_series"] = normalize_season(item.get("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")) item["downloaded_dub_episodes"] = decode_episode_values(item.get("downloaded_dub_episodes_json")) @@ -2027,7 +2027,10 @@ class WatchlistStore: "downloaded_sub_episodes_json": None, "downloaded_dub_episodes_json": None, } - item["auto_download_series"] = normalize_season(payload.get("auto_download_series") or item.get("auto_download_series") or "1") + requested_series = payload.get("auto_download_series") + if requested_series is None or str(requested_series).strip() == "": + requested_series = item.get("auto_download_series") + item["auto_download_series"] = normalize_season(requested_series) requested_mode = str(payload.get("auto_download_mode") or item.get("auto_download_mode") or "").strip().lower() if requested_mode in MODE_CHOICES: item["auto_download_mode"] = requested_mode @@ -2574,7 +2577,10 @@ class WatchlistStore: 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"]) + requested_series = series + if requested_series is None or str(requested_series).strip() == "": + requested_series = existing.get("auto_download_series") + normalized_series = normalize_season(requested_series) normalized_offset = ( normalize_episode_offset(existing.get("auto_download_offset")) if episode_offset is None @@ -2825,7 +2831,7 @@ def download_watchlist_item(show_id, mode): "title": item["title"], "anime_name": item.get("auto_download_name") or item["title"], "media_type": item.get("media_type") or "tv", - "season": item.get("auto_download_series") or "1", + "season": normalize_season(item.get("auto_download_series")), "episode_offset": item.get("auto_download_offset"), "mode": normalized_mode, "quality": item.get("auto_download_quality") @@ -2878,7 +2884,7 @@ def queue_watchlist_auto_download(item, refresh_source="manual"): item.get("auto_download_name") or item.get("title") or "Anime", sanitize_path_component(item.get("title") or "Anime", "Anime"), ) - series = normalize_season(item.get("auto_download_series") or "1") + series = normalize_season(item.get("auto_download_series")) episode_offset = normalize_episode_offset(item.get("auto_download_offset")) jobs = [] for episode_spec in specs: diff --git a/app_support.py b/app_support.py index bbab2ec..b2366c3 100644 --- a/app_support.py +++ b/app_support.py @@ -961,12 +961,14 @@ def sanitize_path_component(value, fallback="Anime"): def normalize_season(value): - text = str(value or "1").strip() + text = "1" if value is None else str(value).strip() + if not text: + text = "1" if not re.match(r"^[0-9]+$", text): - raise ValueError("Season must be a positive number") + raise ValueError("Season must be a non-negative number") number = int(text, 10) - if number < 1: - raise ValueError("Season must be a positive number") + if number < 0: + raise ValueError("Season must be a non-negative number") return str(number) @@ -1001,7 +1003,8 @@ def job_staging_dir(job): def season_label(job): - return f"{int(job.get('season') or 1):02d}" + season = normalize_season(job.get("season")) + return f"{int(season):02d}" def episode_label(value): diff --git a/queue_page.py b/queue_page.py index f3cca48..94be267 100644 --- a/queue_page.py +++ b/queue_page.py @@ -420,7 +420,8 @@ QUEUE_HTML = r""" return `${progress} · ${moved} · ${files} · ${target}`; } const libraryName = job.anime_name || job.title; - const season = String(job.season || "1").padStart(2, "0"); + const rawSeason = job.season === 0 || job.season ? job.season : "1"; + const season = String(rawSeason).padStart(2, "0"); const seasonFolder = `Season ${season}`; const target = job.target_dir || `${job.download_dir}/${libraryName}/${seasonFolder}`; return `${libraryName} · S${season} · ${job.mode} · ${job.quality} · episodes ${job.episodes} · ${target}`; diff --git a/search_page.py b/search_page.py index 7676dfb..acc53fa 100644 --- a/search_page.py +++ b/search_page.py @@ -520,7 +520,7 @@ INDEX_HTML = r"""