tv-anarchy/Sources/TVAnarchyCore/Search/SearchController.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

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 }
}