Expose reusable Jellyfin indexing progress

This commit is contained in:
Sleeps
2026-10-06 17:54:21 +01:00
parent dcd298f5ad
commit d19ae1b736
+143 -27
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import hashlib
import shutil
from collections.abc import Callable
from pathlib import Path
from PIL import Image
@@ -31,6 +32,35 @@ def _library_name(library: dict) -> str:
return str(library.get("Name") or library.get("Id") or "(unnamed)")
ProgressCallback = Callable[[dict], None]
def resolve_library_ids(
client: JellyfinClient,
library_ids: list[str],
) -> list[dict]:
requested = {str(value).strip() for value in library_ids if str(value).strip()}
if not requested:
raise RuntimeError("Select at least one Jellyfin library.")
available = client.media_folders()
by_id = {
str(library.get("Id") or ""): library
for library in available
if str(library.get("Id") or "")
}
missing = sorted(requested.difference(by_id))
if missing:
raise RuntimeError(
"Jellyfin library id(s) not found: " + ", ".join(missing)
)
return sorted(
[by_id[library_id] for library_id in requested],
key=lambda library: _library_name(library).casefold(),
)
def resolve_libraries(
client: JellyfinClient,
requested: list[str] | None,
@@ -197,6 +227,9 @@ def _embed_frames(
frame_paths: list[Path],
media_id: str,
interval_ms: int,
*,
progress: ProgressCallback | None = None,
progress_context: dict | None = None,
) -> tuple[list[tuple], int]:
rows: list[tuple] = []
dim: int | None = None
@@ -232,10 +265,17 @@ def _embed_frames(
)
)
print(
f" embedded "
f"{min(start + len(batch_paths), len(frame_paths))}/{len(frame_paths)}"
)
processed = min(start + len(batch_paths), len(frame_paths))
print(f" embedded {processed}/{len(frame_paths)}")
if progress is not None:
progress(
{
**(progress_context or {}),
"stage": "embedding",
"frames_processed": processed,
"frames_total": len(frame_paths),
}
)
if dim is None:
raise RuntimeError("No frames were embedded")
@@ -276,6 +316,7 @@ def _index_library(
embedder: SiglipEmbedder | None,
limit: int | None,
force: bool,
progress: ProgressCallback | None = None,
) -> tuple[SiglipEmbedder | None, int, int, int, int]:
library_id = str(library["Id"])
library_name = _library_name(library)
@@ -291,12 +332,37 @@ def _index_library(
failed = 0
current_item_ids: set[str] = set()
if progress is not None:
progress(
{
"stage": "library",
"library_id": library_id,
"library": library_name,
"item_number": 0,
"item_total": len(summaries),
}
)
for number, summary in enumerate(summaries, 1):
try:
detail = summary
item_id = str(detail.get("Id") or summary["Id"])
current_item_ids.add(item_id)
title = str(detail.get("Name") or summary.get("Name") or item_id)
if progress is not None:
progress(
{
"stage": "item",
"library_id": library_id,
"library": library_name,
"item_id": item_id,
"item": title,
"item_number": number,
"item_total": len(summaries),
"frames_processed": 0,
"frames_total": 0,
}
)
tags = item_tags(detail)
normalized_tags = {tag.casefold() for tag in tags}
@@ -429,6 +495,15 @@ def _index_library(
frame_paths,
media_id,
interval_ms,
progress=progress,
progress_context={
"library_id": library_id,
"library": library_name,
"item_id": item_id,
"item": title,
"item_number": number,
"item_total": len(summaries),
},
)
size_bytes = int(source.get("Size") or 0)
@@ -501,6 +576,58 @@ def _index_library(
return embedder, indexed, skipped, failed, pruned
def index_libraries(
client: JellyfinClient,
libraries: list[dict],
*,
embedder: SiglipEmbedder | None = None,
limit: int | None = None,
force: bool = False,
progress: ProgressCallback | None = None,
) -> tuple[SiglipEmbedder | None, dict]:
total_indexed = 0
total_skipped = 0
total_failed = 0
total_pruned = 0
for library_number, library in enumerate(libraries, 1):
if progress is not None:
progress(
{
"stage": "library",
"library_id": str(library.get("Id") or ""),
"library": _library_name(library),
"library_number": library_number,
"library_total": len(libraries),
}
)
embedder, indexed, skipped, failed, pruned = _index_library(
client,
library,
embedder=embedder,
limit=limit,
force=force,
progress=progress,
)
total_indexed += indexed
total_skipped += skipped
total_failed += failed
total_pruned += pruned
summary = {
"library_total": len(libraries),
"indexed": total_indexed,
"skipped": total_skipped,
"failed": total_failed,
"pruned": total_pruned,
}
if progress is not None:
progress({"stage": "indexed", **summary})
return embedder, summary
def cmd_jellyfin_index(args: object) -> None:
init_db(settings.db_path)
client = build_jellyfin_client()
@@ -519,30 +646,19 @@ def cmd_jellyfin_index(args: object) -> None:
+ ", ".join(_library_name(library) for library in libraries)
)
embedder: SiglipEmbedder | None = None
total_indexed = 0
total_skipped = 0
total_failed = 0
total_pruned = 0
for library in libraries:
embedder, indexed, skipped, failed, pruned = _index_library(
client,
library,
embedder=embedder,
limit=limit,
force=force,
)
total_indexed += indexed
total_skipped += skipped
total_failed += failed
total_pruned += pruned
_, summary = index_libraries(
client,
libraries,
limit=limit,
force=force,
)
print(
f"\nJellyfin indexing complete across {len(libraries)} librar"
f"{'y' if len(libraries) == 1 else 'ies'}: "
f"{total_indexed} indexed, {total_skipped} skipped, "
f"{total_failed} failed, {total_pruned} pruned."
f"\nJellyfin indexing complete across {summary['library_total']} librar"
f"{'y' if summary['library_total'] == 1 else 'ies'}: "
f"{summary['indexed']} indexed, {summary['skipped']} skipped, "
f"{summary['failed']} failed, {summary['pruned']} pruned."
)
if total_indexed or total_pruned:
if summary["indexed"] or summary["pruned"]:
print("Next: python -m scene_search.cli caption --sync")