48 lines
2.3 KiB
Swift
48 lines
2.3 KiB
Swift
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"]
|
||
?? RepoPaths.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: "'\\''") + "'"
|
||
}
|
||
}
|