Add Jellyfin handoff progress job

This commit is contained in:
Dymas
2026-06-14 13:09:46 +02:00
parent 6403c98b7d
commit 332ec670fc
8 changed files with 475 additions and 35 deletions
+56 -17
View File
@@ -66,7 +66,7 @@ from app_support import (
write_json,
)
from http_handler import HandlerContext, build_handler_class, server_host, server_port
from queue_jobs import DownloadQueue, WatchlistAutoRefreshScheduler, WatchlistRefreshJobs
from queue_jobs import DownloadQueue, JellyfinSyncJobs, WatchlistAutoRefreshScheduler, WatchlistRefreshJobs
from title_matching import (
base_title_variants,
normalize_title_key,
@@ -92,6 +92,7 @@ WATCHLIST = None
DOWNLOAD_QUEUE = None
WATCHLIST_REFRESH = None
WATCHLIST_AUTO_REFRESH = None
WATCHLIST_JELLYFIN_SYNC = None
def graph_request(query, variables, timeout=25):
@@ -1042,15 +1043,28 @@ def watchlist_item_download_complete(item, mode=""):
return True, "ready"
def _move_tree_contents(source_dir, target_dir):
def _move_tree_contents(source_dir, target_dir, progress_fn=None):
moved = []
for source_path in sorted(source_dir.rglob("*"), key=lambda path: (len(path.parts), str(path))):
if source_path.is_dir():
continue
file_paths = [
source_path
for source_path in sorted(source_dir.rglob("*"), key=lambda path: (len(path.parts), str(path)))
if source_path.is_file()
]
total_files = len(file_paths)
for index, source_path in enumerate(file_paths, start=1):
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)
if progress_fn is not None:
progress_fn(
{
"file_index": index,
"file_total": total_files,
"source": str(source_path),
"current_file": str(final_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):
@@ -1085,7 +1099,7 @@ def jellyfin_target_looks_complete(item, target_dir):
return len(files) >= expected_count
def move_watchlist_item_to_jellyfin(item, config):
def move_watchlist_item_to_jellyfin(item, config, progress_fn=None):
source_dir = watchlist_source_library_dir(item, config)
target_dir = jellyfin_target_library_dir(item, config)
if target_dir is None:
@@ -1101,11 +1115,8 @@ def move_watchlist_item_to_jellyfin(item, config):
return {"moved": False, "reason": reason, "item": item, "source": str(source_dir), "target": str(target_dir)}
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)
target_dir.mkdir(parents=True, exist_ok=True)
moved_files = _move_tree_contents(source_dir, target_dir, progress_fn=progress_fn)
return {
"moved": True,
"reason": "moved",
@@ -2558,7 +2569,7 @@ 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):
def trigger_jellyfin_sync_for_item(item, automatic=False, config=None, progress_fn=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}
@@ -2575,7 +2586,7 @@ def trigger_jellyfin_sync_for_item(item, automatic=False, config=None):
debug_log("discord_webhook.jellyfin_sync_failed", show_id=item.get("show_id"), reason=result.get("reason"), error=exc)
return result
try:
result = move_watchlist_item_to_jellyfin(item, active_config)
result = move_watchlist_item_to_jellyfin(item, active_config, progress_fn=progress_fn)
except Exception as exc:
result = {
"moved": False,
@@ -2632,6 +2643,16 @@ def run_jellyfin_sync(config=None):
}
def start_jellyfin_sync(config=None):
ensure_runtime()
return WATCHLIST_JELLYFIN_SYNC.start(config=config)
def get_jellyfin_sync_status():
ensure_runtime()
return WATCHLIST_JELLYFIN_SYNC.status()
def update_watchlist_category(show_id, category):
ensure_runtime()
item = WATCHLIST.update_category(show_id, category)
@@ -2778,6 +2799,7 @@ def runtime_state():
"download_queue": DOWNLOAD_QUEUE,
"watchlist_refresh": WATCHLIST_REFRESH,
"watchlist_auto_refresh": WATCHLIST_AUTO_REFRESH,
"watchlist_jellyfin_sync": WATCHLIST_JELLYFIN_SYNC,
}
@@ -2800,6 +2822,12 @@ def build_runtime(start_workers=None):
config_getter=lambda: get_config_snapshot(config),
start_worker=worker_state,
)
watchlist_jellyfin_sync = JellyfinSyncJobs(
watchlist,
sync_fn=trigger_jellyfin_sync_for_item,
config_getter=lambda: get_config_snapshot(config),
start_worker=worker_state,
)
watchlist_auto_refresh = WatchlistAutoRefreshScheduler(
config_getter=lambda: get_config_snapshot(config),
refresh_start_fn=watchlist_refresh.start,
@@ -2811,11 +2839,12 @@ def build_runtime(start_workers=None):
"download_queue": download_queue,
"watchlist_refresh": watchlist_refresh,
"watchlist_auto_refresh": watchlist_auto_refresh,
"watchlist_jellyfin_sync": watchlist_jellyfin_sync,
}
def initialize_runtime(start_workers=None):
global CONFIG, WATCHLIST, DOWNLOAD_QUEUE, WATCHLIST_REFRESH, WATCHLIST_AUTO_REFRESH
global CONFIG, WATCHLIST, DOWNLOAD_QUEUE, WATCHLIST_REFRESH, WATCHLIST_AUTO_REFRESH, WATCHLIST_JELLYFIN_SYNC
with RUNTIME_LOCK:
workers_allowed = background_workers_enabled()
if (
@@ -2824,6 +2853,7 @@ def initialize_runtime(start_workers=None):
and DOWNLOAD_QUEUE is not None
and WATCHLIST_REFRESH is not None
and WATCHLIST_AUTO_REFRESH is not None
and WATCHLIST_JELLYFIN_SYNC is not None
):
if start_workers and workers_allowed:
WATCHLIST.worker_enabled = True
@@ -2831,6 +2861,7 @@ def initialize_runtime(start_workers=None):
DOWNLOAD_QUEUE.ensure_worker()
WATCHLIST_REFRESH.ensure_worker()
WATCHLIST_AUTO_REFRESH.ensure_worker()
WATCHLIST_JELLYFIN_SYNC.ensure_worker()
return runtime_state()
runtime = build_runtime(start_workers=start_workers)
CONFIG = runtime["config"]
@@ -2838,6 +2869,7 @@ def initialize_runtime(start_workers=None):
DOWNLOAD_QUEUE = runtime["download_queue"]
WATCHLIST_REFRESH = runtime["watchlist_refresh"]
WATCHLIST_AUTO_REFRESH = runtime["watchlist_auto_refresh"]
WATCHLIST_JELLYFIN_SYNC = runtime["watchlist_jellyfin_sync"]
return runtime
@@ -2848,6 +2880,7 @@ def ensure_runtime(start_workers=False):
or DOWNLOAD_QUEUE is None
or WATCHLIST_REFRESH is None
or WATCHLIST_AUTO_REFRESH is None
or WATCHLIST_JELLYFIN_SYNC is None
):
return initialize_runtime(start_workers=start_workers)
if start_workers:
@@ -2864,10 +2897,11 @@ def shutdown_runtime(wait=False, cancel_active_downloads=None):
and DOWNLOAD_QUEUE is None
and WATCHLIST_REFRESH is None
and WATCHLIST_AUTO_REFRESH is None
and WATCHLIST_JELLYFIN_SYNC is None
):
return False
services = (WATCHLIST, DOWNLOAD_QUEUE, WATCHLIST_REFRESH, WATCHLIST_AUTO_REFRESH)
watchlist, download_queue, watchlist_refresh, watchlist_auto_refresh = services
services = (WATCHLIST, DOWNLOAD_QUEUE, WATCHLIST_REFRESH, WATCHLIST_AUTO_REFRESH, WATCHLIST_JELLYFIN_SYNC)
watchlist, download_queue, watchlist_refresh, watchlist_auto_refresh, watchlist_jellyfin_sync = services
should_cancel_downloads = bool(wait) if cancel_active_downloads is None else bool(cancel_active_downloads)
stopped = True
if watchlist is not None:
@@ -2878,11 +2912,13 @@ def shutdown_runtime(wait=False, cancel_active_downloads=None):
stopped = watchlist_refresh.shutdown(wait=wait) and stopped
if watchlist_auto_refresh is not None:
stopped = watchlist_auto_refresh.shutdown(wait=wait) and stopped
if watchlist_jellyfin_sync is not None:
stopped = watchlist_jellyfin_sync.shutdown(wait=wait) and stopped
return stopped
def reset_runtime(wait=False, cancel_active_downloads=None):
global CONFIG, WATCHLIST, DOWNLOAD_QUEUE, WATCHLIST_REFRESH, WATCHLIST_AUTO_REFRESH
global CONFIG, WATCHLIST, DOWNLOAD_QUEUE, WATCHLIST_REFRESH, WATCHLIST_AUTO_REFRESH, WATCHLIST_JELLYFIN_SYNC
stopped = shutdown_runtime(wait=wait, cancel_active_downloads=cancel_active_downloads)
if not stopped:
if wait:
@@ -2894,6 +2930,7 @@ def reset_runtime(wait=False, cancel_active_downloads=None):
DOWNLOAD_QUEUE = None
WATCHLIST_REFRESH = None
WATCHLIST_AUTO_REFRESH = None
WATCHLIST_JELLYFIN_SYNC = None
return True
@@ -2906,6 +2943,7 @@ Handler = build_handler_class(
episode_list=episode_list,
get_config_snapshot=get_config_snapshot,
get_watchlist_homepage_summary=get_watchlist_homepage_summary,
get_jellyfin_sync_status=get_jellyfin_sync_status,
get_watchlist=get_watchlist,
get_watchlist_refresh_status=get_watchlist_refresh_status,
http_error=HttpError,
@@ -2919,6 +2957,7 @@ Handler = build_handler_class(
resolve_search_thumbnail=resolve_search_thumbnail,
test_discord_webhook_config=test_discord_webhook_config,
thumbnail_file_path=thumbnail_file_path,
start_jellyfin_sync=start_jellyfin_sync,
update_all_watchlist_statuses=update_all_watchlist_statuses,
update_watchlist_auto_download=update_watchlist_auto_download,
update_watchlist_category=update_watchlist_category,