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>
104 lines
3.8 KiB
Swift
104 lines
3.8 KiB
Swift
import SwiftUI
|
|
import TVAnarchyCore
|
|
|
|
/// Metadata pipeline: shows each library title with its regex-parsed format
|
|
/// badge (instant, offline) and an Enrich action that resolves TMDB/IMDb,
|
|
/// writes the `.meta` cache, and pushes poster/overview into the Library grid.
|
|
struct MetadataView: View {
|
|
@Bindable var metadata: MetadataController
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
header
|
|
Divider()
|
|
List(metadata.rows) { row in
|
|
MetaRowView(row: row) { Task { await metadata.enrich(row.id) } }
|
|
}
|
|
}
|
|
.onAppear { metadata.reload() }
|
|
}
|
|
|
|
private var header: some View {
|
|
HStack(spacing: 12) {
|
|
Text("Metadata").font(.title2).bold()
|
|
Spacer()
|
|
if let msg = metadata.lastMessage {
|
|
Text(msg).font(.caption).foregroundStyle(.secondary).lineLimit(1)
|
|
.onTapGesture { copyToClipboard(msg) }.help("Click to copy")
|
|
}
|
|
if metadata.busy { ProgressView().controlSize(.small) }
|
|
Button { Task { await metadata.enrichAll() } } label: {
|
|
Label("Enrich all", systemImage: "wand.and.stars")
|
|
}
|
|
.disabled(metadata.busy || metadata.rows.isEmpty)
|
|
}
|
|
.padding(16)
|
|
}
|
|
}
|
|
|
|
private struct MetaRowView: View {
|
|
let row: MetadataController.Row
|
|
let onEnrich: () -> Void
|
|
|
|
var body: some View {
|
|
HStack(alignment: .top, spacing: 12) {
|
|
PosterThumb(urlString: row.meta?.posterURL).frame(width: 46, height: 69)
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(row.meta?.resolvedTitle ?? row.show.name).font(.headline).lineLimit(1)
|
|
badges
|
|
if let overview = row.meta?.overview, !overview.isEmpty {
|
|
Text(overview).font(.caption).foregroundStyle(.secondary).lineLimit(2)
|
|
}
|
|
}
|
|
Spacer()
|
|
if row.enriching {
|
|
ProgressView().controlSize(.small)
|
|
} else {
|
|
Button(row.meta == nil ? "Enrich" : "Re-enrich", action: onEnrich)
|
|
.buttonStyle(.bordered)
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
|
|
private var badges: some View {
|
|
HStack(spacing: 6) {
|
|
if let s = row.sample {
|
|
if let q = s.quality { tag(q.uppercased(), .blue) }
|
|
if let c = s.codec { tag(c.uppercased(), .purple) }
|
|
}
|
|
if !row.show.countSummary.isEmpty {
|
|
Text(row.show.countSummary).font(.caption2).foregroundStyle(.secondary)
|
|
}
|
|
if let r = row.meta?.imdbRating { tag(String(format: "IMDb %.1f", r), .yellow) }
|
|
if let genres = row.meta?.genres, let g = genres.first { tag(g, .gray) }
|
|
}
|
|
}
|
|
|
|
private func tag(_ text: String, _ color: Color) -> some View {
|
|
Text(text)
|
|
.font(.caption2.monospacedDigit())
|
|
.padding(.horizontal, 6).padding(.vertical, 2)
|
|
.background(color.opacity(0.2), in: Capsule())
|
|
.foregroundStyle(color)
|
|
}
|
|
}
|
|
|
|
/// Small poster that fetches a remote TMDB URL (or shows a placeholder).
|
|
private struct PosterThumb: View {
|
|
let urlString: String?
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
RoundedRectangle(cornerRadius: 6).fill(.quaternary)
|
|
if let s = urlString, let url = URL(string: s) {
|
|
AsyncImage(url: url) { img in
|
|
img.resizable().aspectRatio(contentMode: .fill)
|
|
} placeholder: { ProgressView().controlSize(.small) }
|
|
.clipShape(RoundedRectangle(cornerRadius: 6))
|
|
} else {
|
|
Image(systemName: "film").foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|