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>
70 lines
2.9 KiB
Swift
70 lines
2.9 KiB
Swift
import Foundation
|
|
import Observation
|
|
|
|
/// The unified finder behind the Search tab. One query answers "do I have it,
|
|
/// am I getting it, can I get it?" by matching three sources:
|
|
/// • the scanned library (owned),
|
|
/// • live transmission transfers (in flight / done),
|
|
/// • torrent search results (available to download).
|
|
/// Library + transfer facets are instant (local); torrents are the async leg.
|
|
@Observable
|
|
@MainActor
|
|
public final class SearchController {
|
|
public var query: String = ""
|
|
public private(set) var libraryMatches: [CachedShow] = []
|
|
public private(set) var transferMatches: [TorrentRow] = []
|
|
public private(set) var torrentResults: [TorrentResult] = []
|
|
public private(set) var searching = false
|
|
public private(set) var torrentError: String?
|
|
public private(set) var lastAction: String?
|
|
public private(set) var didSearch = false
|
|
|
|
private let library: LibraryController
|
|
private let downloads: DownloadsController
|
|
private let torrents: TorrentService
|
|
|
|
public init(library: LibraryController, downloads: DownloadsController,
|
|
torrents: TorrentService = TorrentService()) {
|
|
self.library = library; self.downloads = downloads; self.torrents = torrents
|
|
}
|
|
|
|
public func search() async {
|
|
let q = query.trimmingCharacters(in: .whitespaces)
|
|
guard !q.isEmpty, !searching else { return }
|
|
let lower = q.lowercased()
|
|
didSearch = true
|
|
|
|
// Instant local facets.
|
|
libraryMatches = library.shows
|
|
.filter { $0.name.lowercased().contains(lower) }
|
|
.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
|
|
transferMatches = downloads.transfers.filter { $0.name.lowercased().contains(lower) }
|
|
|
|
// Async torrent leg.
|
|
searching = true
|
|
torrentError = nil
|
|
defer { searching = false }
|
|
do {
|
|
torrentResults = try await torrents.search(q, limit: 25)
|
|
if torrentResults.isEmpty {
|
|
torrentError = "No torrents found (FlareSolverr/Playwright may be down)."
|
|
}
|
|
} catch {
|
|
torrentResults = []
|
|
torrentError = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
|
|
}
|
|
}
|
|
|
|
/// Add a torrent result (optionally routed to a category) via the downloads
|
|
/// controller, and refresh the in-flight facet so it shows immediately.
|
|
public func add(_ result: TorrentResult, category: String?) async {
|
|
await downloads.add(result, category: category)
|
|
lastAction = downloads.lastAction
|
|
let lower = query.trimmingCharacters(in: .whitespaces).lowercased()
|
|
if !lower.isEmpty { transferMatches = downloads.transfers.filter { $0.name.lowercased().contains(lower) } }
|
|
}
|
|
|
|
/// True when the library already has something matching — used to warn before
|
|
/// re-downloading.
|
|
public var ownsMatch: Bool { !libraryMatches.isEmpty }
|
|
}
|