tv-anarchy/Sources/TVAnarchyCore/Metadata/EnrichService.swift
Natalie 92b38b1bae refactor(tv-anarchy): rename PlumTV→TVAnarchy and land session work
Renames Sources/PlumTV→TVAnarchy and PlumTVCore→TVAnarchyCore (the rename
the auto-commit service couldn't stage — it git-add'd the old, now-gone
paths and aborted every cycle), and commits the accumulated work:

- Library: black-built index fast path (LibraryIndex + scanFromIndex) with
  NFS-walk fallback; incremental --add on download-complete; mtime staleness
  gate; loose-file series-collapse fix; determinate scan/index progress.
- Cover art: keyless TVmaze cartoon-vs-live-action disambiguation (type/year).
- Player: sleep timer (timed + end-of-episode); visibility-gated polling.
- Home: Continue Watching cover art + live refresh; Recently Added; adult gate.
- Logs: multi-line selection + copy; truncated giant tx-list errors.
- Hover previews (opt-in) via black ffmpeg + scp.

Also gitignores foreign project trees (governor/mcp/fleet/recommender) that
sit in this directory but belong to their own repos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:04:22 -07:00

49 lines
2.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
public enum EnrichError: Error, LocalizedError {
case failed(String)
public var errorDescription: String? { if case let .failed(m) = self { return m }; return nil }
}
/// Resolves a title against TMDB + the local IMDb index by shelling into
/// media-recommender's `enrich` module (single source of truth for the
/// TMDB/IMDb pipeline). Runs under a login shell so `uv` is on PATH. The CLI
/// degrades gracefully, so a result with only IMDb fields is normal and useful.
public final class EnrichService: Sendable {
private let projectDir: String
public init(projectDir: String? = nil) {
self.projectDir = projectDir
?? ProcessInfo.processInfo.environment["MEDIA_RECOMMENDER_DIR"]
?? FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Code/@applications/media-recommender").path
}
public func enrich(title: String, year: Int?, category: String? = nil) async throws -> EnrichResult {
var inner = ["uv", "run", "python", "-m", "media_rec.enrich", title]
if let year { inner.append(String(year)) }
// Route the keyless provider (anime AniList, tv/cartoons TVmaze).
if let category, !category.isEmpty { inner.append(contentsOf: ["--category", category]) }
let command = "cd \(shq(projectDir)) && " + inner.map(shq).joined(separator: " ")
let dir = projectDir
let r: ProcessResult = await Task.detached(priority: .utility) {
ProcessRunner.runShell(command, timeout: 60, cwd: dir)
}.value
guard r.ok else {
let detail = r.stderr.split(separator: "\n").last.map(String.init) ?? r.stderr
Log.error("enrich \(title) [\(category ?? "-")] failed (exit \(r.status)): \(r.stderr.suffix(300))")
throw EnrichError.failed(detail.isEmpty ? "enrich exited \(r.status)" : detail)
}
Log.info("enrich \(title) [\(category ?? "-")] ok")
guard let data = r.stdout.trimmingCharacters(in: .whitespacesAndNewlines).data(using: .utf8),
!data.isEmpty else {
throw EnrichError.failed("enrich produced no output")
}
do { return try JSONDecoder().decode(EnrichResult.self, from: data) }
catch { throw EnrichError.failed("decode failed: \(error.localizedDescription)") }
}
private func shq(_ s: String) -> String {
"'" + s.replacingOccurrences(of: "'", with: "'\\''") + "'"
}
}