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>
59 lines
No EOL
3 KiB
Swift
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
|
|
}
|
|
} |