tv-anarchy/Sources/TVAnarchyCore/Search/SearchMatcher.swift
Natalie 4a2ceb9781 feat(offline): inline star-to-keep and trash-to-cull on cache rows
Surface the existing pin (keep-from-cull) and per-file delete actions as
visible inline buttons on each offline cache row instead of context-menu-only:
a star toggles protection from auto-cull (and restore-if-missing), a trash
culls that file early. Aligns wording/icons to the star metaphor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 00:12:41 -04:00

59 lines
No EOL
3 KiB
Swift

import Foundation
/// Cross-facet overlap checks for the unified Search tab library, transfers,
/// and torrent results should not surface the same work twice.
public enum SearchMatcher {
/// Stable title key for fuzzy overlap (regex-first, then normalized folder name).
public static func titleKey(_ name: String) -> String {
let parsed = FilenameParser.parse(filename: name)
let trimmed = parsed.title.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.count >= 2 { return trimmed.lowercased() }
return LibraryScanner.normalizeShowName(name).lowercased()
}
public static func titlesOverlap(_ a: String, _ b: String) -> Bool {
let ka = titleKey(a), kb = titleKey(b)
guard !ka.isEmpty, !kb.isEmpty else { return false }
return ka == kb || ka.contains(kb) || kb.contains(ka)
}
/// True when a library show (or its episodes) already covers this torrent hit.
public static func overlapsLibrary(_ result: TorrentResult, shows: [CachedShow], query: String) -> Bool {
let q = query.trimmingCharacters(in: .whitespaces).lowercased()
let rk = titleKey(result.filename)
return shows.contains { show in
let sn = show.name.lowercased()
if !q.isEmpty, sn.contains(q) { return titlesOverlap(result.filename, show.name) }
if titlesOverlap(result.filename, show.name) { return true }
return show.episodes.contains { ep in
titlesOverlap(result.filename, ep.label) || ep.label.lowercased().contains(rk)
}
}
}
/// True when an active or completed transfer already covers this torrent hit.
public static func overlapsTransfer(_ result: TorrentResult, transfers: [TorrentRow]) -> Bool {
let rSeason = FilenameParser.parse(filename: result.filename).season
return transfers.contains { row in
guard titlesOverlap(result.filename, row.name) else { return false }
let tSeason = FilenameParser.parse(filename: row.name).season
if let rs = rSeason, let ts = tSeason { return rs == ts }
if rSeason != nil || tSeason != nil { return rSeason == tSeason }
return true
}
}
/// A TV torrent filed under a parent folder that doesn't mention its title
/// (e.g. Broad City inside a South Park repair dir) library grouping breaks.
public static func isMisplaced(_ row: TorrentRow) -> Bool {
guard let dir = row.downloadDir?.lowercased() else { return false }
let title = FilenameParser.parse(filename: row.name).title.lowercased()
guard title.count >= 3 else { return false }
if dir.contains(title) { return false }
let compact = title.replacingOccurrences(of: " ", with: "")
if !compact.isEmpty, dir.contains(compact) { return false }
let parsed = FilenameParser.parse(filename: row.name)
return parsed.season != nil || parsed.episode != nil
|| row.name.range(of: "\\bseason\\b", options: [.regularExpression, .caseInsensitive]) != nil
}
}