Add Jellyfin library handoff controls
This commit is contained in:
@@ -919,6 +919,100 @@ def watchlist_status_message(sub_count, dub_count, expected_count=0, airing_stat
|
||||
return ". ".join(parts) + "."
|
||||
|
||||
|
||||
def watchlist_download_library_name(item):
|
||||
return sanitize_path_component(
|
||||
item.get("auto_download_name") or item.get("title") or item.get("show_id") or "Anime",
|
||||
sanitize_path_component(item.get("title") or item.get("show_id") or "Anime", "Anime"),
|
||||
)
|
||||
|
||||
|
||||
def watchlist_source_library_dir(item, config):
|
||||
media_type = normalize_media_type(item.get("media_type"))
|
||||
return Path(str((config or {}).get("download_dir") or DEFAULT_CONFIG["download_dir"])).expanduser() / media_type / watchlist_download_library_name(item)
|
||||
|
||||
|
||||
def jellyfin_target_library_dir(item, config):
|
||||
media_type = normalize_media_type(item.get("media_type"))
|
||||
key = "jellyfin_movie_dir" if media_type == "movie" else "jellyfin_tv_dir"
|
||||
root = str((config or {}).get(key) or "").strip()
|
||||
if not root:
|
||||
return None
|
||||
return Path(root).expanduser() / watchlist_download_library_name(item)
|
||||
|
||||
|
||||
def watchlist_item_ready_for_jellyfin(item):
|
||||
if not isinstance(item, dict):
|
||||
return False, "invalid_item"
|
||||
if not bool(item.get("downloaded")):
|
||||
return False, "not_downloaded"
|
||||
if normalize_watchlist_category(item.get("category")) != "finished":
|
||||
return False, "category"
|
||||
if normalize_media_type(item.get("media_type")) == "movie":
|
||||
return True, "ready"
|
||||
expected_count = int(item.get("expected_count") or 0)
|
||||
if expected_count < 1:
|
||||
return False, "expected_count"
|
||||
if normalize_animeschedule_status(item.get("airing_status")) != "Finished":
|
||||
return False, "airing_status"
|
||||
downloaded_sub = len(item.get("downloaded_sub_episodes") or [])
|
||||
downloaded_dub = len(item.get("downloaded_dub_episodes") or [])
|
||||
if max(downloaded_sub, downloaded_dub) < expected_count:
|
||||
return False, "episodes_missing"
|
||||
return True, "ready"
|
||||
|
||||
|
||||
def _move_tree_contents(source_dir, target_dir):
|
||||
moved = []
|
||||
for source_path in sorted(source_dir.rglob("*"), key=lambda path: (len(path.parts), str(path))):
|
||||
if source_path.is_dir():
|
||||
continue
|
||||
relative = source_path.relative_to(source_dir)
|
||||
destination = target_dir / relative
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
final_destination = app_support.unique_destination(destination)
|
||||
shutil.move(str(source_path), str(final_destination))
|
||||
moved.append(str(final_destination))
|
||||
for path in sorted(source_dir.rglob("*"), key=lambda path: len(path.parts), reverse=True):
|
||||
if path.is_dir():
|
||||
try:
|
||||
path.rmdir()
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
source_dir.rmdir()
|
||||
except OSError:
|
||||
pass
|
||||
return moved
|
||||
|
||||
|
||||
def move_watchlist_item_to_jellyfin(item, config):
|
||||
source_dir = watchlist_source_library_dir(item, config)
|
||||
target_dir = jellyfin_target_library_dir(item, config)
|
||||
if target_dir is None:
|
||||
return {"moved": False, "reason": "target_missing", "item": item}
|
||||
try:
|
||||
if source_dir.resolve() == target_dir.resolve():
|
||||
return {"moved": False, "reason": "same_path", "item": item, "source": str(source_dir), "target": str(target_dir)}
|
||||
except OSError:
|
||||
pass
|
||||
if not source_dir.exists():
|
||||
return {"moved": False, "reason": "source_missing", "item": item, "source": str(source_dir), "target": str(target_dir)}
|
||||
target_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not target_dir.exists():
|
||||
shutil.move(str(source_dir), str(target_dir))
|
||||
moved_files = [str(path) for path in target_dir.rglob("*") if path.is_file()]
|
||||
else:
|
||||
moved_files = _move_tree_contents(source_dir, target_dir)
|
||||
return {
|
||||
"moved": True,
|
||||
"reason": "moved",
|
||||
"item": item,
|
||||
"source": str(source_dir),
|
||||
"target": str(target_dir),
|
||||
"moved_files": moved_files,
|
||||
}
|
||||
|
||||
|
||||
def episode_specs_for_values(values, available_episodes):
|
||||
ordered = [str(value).strip() for value in available_episodes or [] if str(value).strip()]
|
||||
targets = {str(value).strip() for value in values or [] if str(value).strip()}
|
||||
@@ -1003,6 +1097,7 @@ class WatchlistStore:
|
||||
self.lock = threading.RLock()
|
||||
self.config_getter = config_getter or (lambda: dict(DEFAULT_CONFIG))
|
||||
self.auto_download_fn = None
|
||||
self.jellyfin_sync_fn = None
|
||||
self.thumbnail_events = {}
|
||||
self.refresh_pending = []
|
||||
self.refresh_pending_set = set()
|
||||
@@ -1736,6 +1831,11 @@ class WatchlistStore:
|
||||
item["auto_download_result"] = self.auto_download_fn(item, refresh_source=refresh_source)
|
||||
except Exception as exc:
|
||||
debug_log("watchlist.auto_download.error", show_id=normalized_show_id, source=refresh_source, error=exc)
|
||||
if self.jellyfin_sync_fn is not None:
|
||||
try:
|
||||
item["jellyfin_sync_result"] = self.jellyfin_sync_fn(item, automatic=True)
|
||||
except Exception as exc:
|
||||
debug_log("watchlist.jellyfin_sync.error", show_id=normalized_show_id, source=refresh_source, error=exc)
|
||||
return item
|
||||
|
||||
def refresh_all(self):
|
||||
@@ -2214,6 +2314,61 @@ def queue_watchlist_auto_download(item, refresh_source="manual"):
|
||||
return {"queued": True, "mode": mode, "quality": quality, "episodes": specs, "jobs": jobs}
|
||||
|
||||
|
||||
def trigger_jellyfin_sync_for_item(item, automatic=False, config=None):
|
||||
active_config = normalize_config(config or get_config_snapshot())
|
||||
if automatic and not bool(active_config.get("jellyfin_sync_enabled")):
|
||||
return {"moved": False, "reason": "disabled", "item": item}
|
||||
ready, reason = watchlist_item_ready_for_jellyfin(item)
|
||||
if not ready:
|
||||
return {"moved": False, "reason": reason, "item": item}
|
||||
media_type = normalize_media_type(item.get("media_type"))
|
||||
target_key = "jellyfin_movie_dir" if media_type == "movie" else "jellyfin_tv_dir"
|
||||
if not str(active_config.get(target_key) or "").strip():
|
||||
return {"moved": False, "reason": "target_missing", "item": item}
|
||||
result = move_watchlist_item_to_jellyfin(item, active_config)
|
||||
if result.get("moved"):
|
||||
debug_log(
|
||||
"jellyfin.sync.moved",
|
||||
show_id=item.get("show_id"),
|
||||
media_type=media_type,
|
||||
source=result.get("source"),
|
||||
target=result.get("target"),
|
||||
automatic=automatic,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def run_jellyfin_sync(config=None):
|
||||
runtime = ensure_runtime()
|
||||
active_config = normalize_config(config or runtime["config"] or {})
|
||||
items = []
|
||||
moved = 0
|
||||
for show_id in runtime["watchlist"].all_show_ids():
|
||||
try:
|
||||
item = runtime["watchlist"].get(show_id)
|
||||
except KeyError:
|
||||
continue
|
||||
result = trigger_jellyfin_sync_for_item(item, automatic=False, config=active_config)
|
||||
items.append(
|
||||
{
|
||||
"show_id": item.get("show_id"),
|
||||
"title": item.get("title"),
|
||||
"media_type": item.get("media_type"),
|
||||
"moved": bool(result.get("moved")),
|
||||
"reason": result.get("reason"),
|
||||
"target": result.get("target"),
|
||||
}
|
||||
)
|
||||
if result.get("moved"):
|
||||
moved += 1
|
||||
return {
|
||||
"message": f"Moved {moved} watchlist entr{'y' if moved == 1 else 'ies'} to Jellyfin libraries.",
|
||||
"moved": moved,
|
||||
"total": len(items),
|
||||
"items": items,
|
||||
}
|
||||
|
||||
|
||||
def update_watchlist_category(show_id, category):
|
||||
ensure_runtime()
|
||||
item = WATCHLIST.update_category(show_id, category)
|
||||
@@ -2353,6 +2508,7 @@ def build_runtime(start_workers=None):
|
||||
start_worker=worker_state,
|
||||
)
|
||||
watchlist.auto_download_fn = queue_watchlist_auto_download
|
||||
watchlist.jellyfin_sync_fn = trigger_jellyfin_sync_for_item
|
||||
watchlist_refresh = WatchlistRefreshJobs(
|
||||
watchlist,
|
||||
config_getter=lambda: get_config_snapshot(config),
|
||||
@@ -2469,6 +2625,7 @@ Handler = build_handler_class(
|
||||
index_html=INDEX_HTML,
|
||||
queue_html=QUEUE_HTML,
|
||||
remove_from_watchlist=remove_from_watchlist,
|
||||
run_jellyfin_sync=run_jellyfin_sync,
|
||||
runtime_state=runtime_state,
|
||||
save_runtime_config=save_runtime_config,
|
||||
search_anime=search_anime,
|
||||
|
||||
Reference in New Issue
Block a user