Enhance project structure and add new files for enrichment and analysis
- Updated AGENTS.md to reflect changes in CLI commands and module organization, including the addition of an enrichment step and new functional modules. - Introduced analysis.json, identities.json, inventory.csv, and plan.json to support enriched metadata and execution planning. - Added CODE_IMPROVEMENTS.md to document identified code issues and proposed solutions for future enhancements. - Updated README.md to include new enrichment features and configuration options. - Removed unused dependency on ffmpeg-python from pyproject.toml. These changes improve the overall functionality and maintainability of the Video Library Manager project.
This commit is contained in:
+112
-26
@@ -12,7 +12,7 @@ from urllib.request import urlopen, Request
|
||||
|
||||
from vlm.cache import EnrichmentCache
|
||||
from vlm.config import Config
|
||||
from vlm.providers import ProviderResult, TMDBProvider
|
||||
from vlm.providers import ProviderResult, TMDBAuthError, TMDBProvider, TMDBProviderError
|
||||
from vlm.parser import normalize_title
|
||||
|
||||
RefreshMode = str
|
||||
@@ -59,6 +59,7 @@ def enrich_identities_data(
|
||||
"failed": 0,
|
||||
"api_calls": 0,
|
||||
"failed_items": [],
|
||||
"skip_reasons": {},
|
||||
}
|
||||
|
||||
refresh_all = refresh_mode == "refresh_all"
|
||||
@@ -69,6 +70,7 @@ def enrich_identities_data(
|
||||
title = record.get("title") or _fallback_title_from_filename(record.get("filename"))
|
||||
if not title:
|
||||
stats["skipped"] = int(stats["skipped"]) + 1
|
||||
_increment_skip_reason(stats, "invalid_input")
|
||||
stats["processed"] = int(stats["processed"]) + 1
|
||||
_emit_progress(stats, progress_callback)
|
||||
continue
|
||||
@@ -92,7 +94,7 @@ def enrich_identities_data(
|
||||
_emit_progress(stats, progress_callback)
|
||||
continue
|
||||
|
||||
payload, api_calls, failures = _enrich_record(
|
||||
payload, api_calls, failures, skip_reason = _enrich_record(
|
||||
record,
|
||||
media_type,
|
||||
providers,
|
||||
@@ -100,26 +102,12 @@ def enrich_identities_data(
|
||||
request_timeout=request_timeout,
|
||||
retries=retries,
|
||||
)
|
||||
stats["api_calls"] = int(stats["api_calls"]) + api_calls
|
||||
if failures:
|
||||
failed_items = stats["failed_items"]
|
||||
assert isinstance(failed_items, list)
|
||||
failed_items.extend(failures)
|
||||
stats["failed"] = int(stats["failed"]) + len(failures)
|
||||
|
||||
_apply_payload(record, payload)
|
||||
cache.put_identity(identity_key, fingerprint, payload)
|
||||
|
||||
if payload.get("enriched"):
|
||||
stats["enriched"] = int(stats["enriched"]) + 1
|
||||
else:
|
||||
stats["skipped"] = int(stats["skipped"]) + 1
|
||||
|
||||
if record.get("needs_review"):
|
||||
stats["needs_review"] = int(stats["needs_review"]) + 1
|
||||
|
||||
stats["processed"] = int(stats["processed"]) + 1
|
||||
_emit_progress(stats, progress_callback)
|
||||
_update_stats_after_enrich(
|
||||
stats, payload, failures, api_calls, record, progress_callback, skip_reason
|
||||
)
|
||||
|
||||
metadata = identities_data.setdefault("metadata", {})
|
||||
metadata["enriched"] = True
|
||||
@@ -154,6 +142,33 @@ def _emit_progress(stats: dict[str, int | list[dict[str, str]]], progress_callba
|
||||
)
|
||||
|
||||
|
||||
def _update_stats_after_enrich(
|
||||
stats: dict[str, int | list[dict[str, str]]],
|
||||
payload: dict,
|
||||
failures: list[dict[str, str]],
|
||||
api_calls: int,
|
||||
record: dict,
|
||||
progress_callback: Optional[ProgressCallback],
|
||||
skip_reason: str,
|
||||
) -> None:
|
||||
"""Update stats and emit progress after enriching a single record."""
|
||||
stats["api_calls"] = int(stats["api_calls"]) + api_calls
|
||||
if failures:
|
||||
failed_items = stats["failed_items"]
|
||||
assert isinstance(failed_items, list)
|
||||
failed_items.extend(failures)
|
||||
stats["failed"] = int(stats["failed"]) + len(failures)
|
||||
if payload.get("enriched"):
|
||||
stats["enriched"] = int(stats["enriched"]) + 1
|
||||
else:
|
||||
stats["skipped"] = int(stats["skipped"]) + 1
|
||||
_increment_skip_reason(stats, skip_reason or "no_match")
|
||||
if record.get("needs_review"):
|
||||
stats["needs_review"] = int(stats["needs_review"]) + 1
|
||||
stats["processed"] = int(stats["processed"]) + 1
|
||||
_emit_progress(stats, progress_callback)
|
||||
|
||||
|
||||
def _build_providers(config: Config, *, request_timeout: int, retries: int) -> list:
|
||||
providers = []
|
||||
unsupported: list[str] = []
|
||||
@@ -163,6 +178,10 @@ def _build_providers(config: Config, *, request_timeout: int, retries: int) -> l
|
||||
providers.append(
|
||||
TMDBProvider(
|
||||
config.tmdb_api_key,
|
||||
bearer_token=config.tmdb_bearer_token,
|
||||
language=config.tmdb_language,
|
||||
region=config.tmdb_region,
|
||||
include_adult=config.tmdb_include_adult,
|
||||
timeout_seconds=request_timeout,
|
||||
retries=retries,
|
||||
min_interval_seconds=0.25,
|
||||
@@ -187,37 +206,57 @@ def _enrich_record(
|
||||
*,
|
||||
request_timeout: int,
|
||||
retries: int,
|
||||
) -> tuple[dict, int, list[dict[str, str]]]:
|
||||
) -> tuple[dict, int, list[dict[str, str]], str]:
|
||||
title = record.get("title")
|
||||
year = record.get("year") if media_type == "movie" else None
|
||||
|
||||
provider_results: list[ProviderResult] = []
|
||||
failures: list[dict[str, str]] = []
|
||||
api_calls = 0
|
||||
configured_provider_count = 0
|
||||
|
||||
for provider in providers:
|
||||
api_calls += 1
|
||||
if not _provider_is_configured(provider, config):
|
||||
continue
|
||||
|
||||
configured_provider_count += 1
|
||||
try:
|
||||
result = provider.enrich(title=title, media_type=media_type, year=year)
|
||||
except TMDBAuthError as exc:
|
||||
raise RuntimeError(str(exc)) from exc
|
||||
except TMDBProviderError as exc:
|
||||
failures.append(
|
||||
{
|
||||
"path": str(record.get("path", "")),
|
||||
"title": str(title),
|
||||
"provider": provider.name,
|
||||
"reason": str(exc),
|
||||
}
|
||||
)
|
||||
api_calls += provider.last_request_count
|
||||
continue
|
||||
except Exception as exc:
|
||||
failures.append(
|
||||
{
|
||||
"path": str(record.get("path", "")),
|
||||
"title": str(title),
|
||||
"provider": getattr(provider, "name", "unknown"),
|
||||
"provider": provider.name,
|
||||
"reason": str(exc),
|
||||
}
|
||||
)
|
||||
api_calls += provider.last_request_count
|
||||
continue
|
||||
|
||||
api_calls += provider.last_request_count
|
||||
if result:
|
||||
provider_results.append(result)
|
||||
|
||||
merged = _merge_provider_results(provider_results)
|
||||
|
||||
# Optional AI fallback for missing translated titles.
|
||||
if config.translation_fallback_machine:
|
||||
if config.translation_fallback_machine and config.openai_api_key:
|
||||
if not merged.get("title_zh"):
|
||||
api_calls += 1
|
||||
translated = _translate_with_openai(
|
||||
title,
|
||||
target_language="Chinese (Simplified)",
|
||||
@@ -225,12 +264,12 @@ def _enrich_record(
|
||||
timeout_seconds=request_timeout,
|
||||
retries=retries,
|
||||
)
|
||||
api_calls += 1
|
||||
if translated:
|
||||
merged["title_zh"] = translated
|
||||
merged["translation_source"] = merged.get("translation_source") or "openai"
|
||||
|
||||
if not merged.get("title_en"):
|
||||
api_calls += 1
|
||||
translated = _translate_with_openai(
|
||||
title,
|
||||
target_language="English",
|
||||
@@ -238,7 +277,6 @@ def _enrich_record(
|
||||
timeout_seconds=request_timeout,
|
||||
retries=retries,
|
||||
)
|
||||
api_calls += 1
|
||||
if translated:
|
||||
merged["title_en"] = translated
|
||||
merged["translation_source"] = merged.get("translation_source") or "openai"
|
||||
@@ -266,7 +304,55 @@ def _enrich_record(
|
||||
merged["enriched"] = bool(provider_results or merged.get("translation_source"))
|
||||
merged["display_title"] = _build_display_title(record, merged, config)
|
||||
|
||||
return merged, api_calls, failures
|
||||
skip_reason = _determine_skip_reason(
|
||||
provider_results=provider_results,
|
||||
failures=failures,
|
||||
configured_provider_count=configured_provider_count,
|
||||
api_calls=api_calls,
|
||||
)
|
||||
|
||||
return merged, api_calls, failures, skip_reason
|
||||
|
||||
|
||||
def _determine_skip_reason(
|
||||
*,
|
||||
provider_results: list[ProviderResult],
|
||||
failures: list[dict[str, str]],
|
||||
configured_provider_count: int,
|
||||
api_calls: int,
|
||||
) -> str:
|
||||
if provider_results:
|
||||
return ""
|
||||
if configured_provider_count == 0 and api_calls == 0:
|
||||
return "no_key"
|
||||
if failures:
|
||||
for failure in failures:
|
||||
reason = str(failure.get("reason", "")).lower()
|
||||
if "rate limit" in reason or "(429)" in reason:
|
||||
return "rate_limited"
|
||||
if "authentication failed" in reason or "(401/403)" in reason:
|
||||
return "auth_error"
|
||||
return "provider_error"
|
||||
return "no_match"
|
||||
|
||||
|
||||
def _increment_skip_reason(stats: dict[str, int | list[dict[str, str]]], reason: str) -> None:
|
||||
if not reason:
|
||||
return
|
||||
current = stats.get("skip_reasons")
|
||||
if not isinstance(current, dict):
|
||||
current = {}
|
||||
stats["skip_reasons"] = current
|
||||
current[reason] = int(current.get(reason, 0)) + 1
|
||||
|
||||
|
||||
def _provider_is_configured(provider: object, config: Config) -> bool:
|
||||
provider_name = provider.name.lower()
|
||||
|
||||
if provider_name == "tmdb":
|
||||
return bool(config.tmdb_bearer_token or config.tmdb_api_key)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _merge_provider_results(results: list[ProviderResult]) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user