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

72 lines
3.2 KiB
Swift

import Foundation
/// Audio vs subtitle track.
public enum TrackKind: String, Sendable, Equatable { case audio, subtitle }
/// One selectable track on the currently-playing file. `id` is the backend's
/// track id (mpv aid/sid, VLC es id).
public struct MediaTrack: Identifiable, Sendable, Equatable {
public let id: Int
public let kind: TrackKind
public let lang: String?
public let title: String?
public let codec: String?
public let selected: Bool
public init(id: Int, kind: TrackKind, lang: String? = nil, title: String? = nil,
codec: String? = nil, selected: Bool = false) {
self.id = id; self.kind = kind; self.lang = lang
self.title = title; self.codec = codec; self.selected = selected
}
/// Human label for a menu row, e.g. "JPN · Dialogue", "ENG", "Track 2".
public var label: String {
var parts: [String] = []
if let lang, !lang.isEmpty { parts.append(lang.uppercased()) }
if let title, !title.isEmpty { parts.append(title) }
else if let codec, !codec.isEmpty { parts.append(codec.uppercased()) }
let s = parts.joined(separator: " · ")
return s.isEmpty ? "Track \(id)" : s
}
}
/// Per-series audio/subtitle intent. `sub` = original audio + subtitles on (the
/// common anime case); `dub` = native-language audio + subtitles off.
public enum DubSub: String, Sendable, Codable, CaseIterable, Identifiable {
case sub, dub
public var id: String { rawValue }
public var label: String { self == .sub ? "Sub" : "Dub" }
/// Preferred audio languages (mpv alang order). For `sub` we want the original
/// (Japanese) audio; for `dub` the English track. mpv falls back to the only
/// available track when none match, so English-native content stays correct.
public var audioLangs: [String] {
switch self {
case .sub: return ["jpn", "ja", "jp"]
case .dub: return ["eng", "en"]
}
}
/// Preferred subtitle languages (mpv slang order).
public var subLangs: [String] { ["eng", "en"] }
/// Whether subtitles should be shown at all.
public var subsEnabled: Bool { self == .sub }
}
/// A target that can enumerate and select audio/subtitle tracks. Language
/// preference persists across files (mpv `alang`/`slang`, applied to every file
/// the playlist loads next); explicit track ids are per-file.
public protocol TrackSelectable: AnyObject {
func tracks() async -> [MediaTrack]
func setAudioTrack(_ id: Int) async
func setSubtitleTrack(_ id: Int?) async // nil = off
/// Persist a language preference (carries to subsequent files) and best-effort
/// select matching tracks on the currently-loaded file.
func applyLanguagePreference(audioLangs: [String], subLangs: [String], subsEnabled: Bool) async
}
/// True when `lang` matches any of `wanted` (case-insensitive prefix, so "jpn"
/// matches "jp"/"jpn"/"ja" loosely). Shared by the mpv/VLC track matchers.
public func trackLangMatches(_ lang: String?, _ wanted: [String]) -> Bool {
guard let lang = lang?.lowercased(), !lang.isEmpty else { return false }
return wanted.contains { lang.hasPrefix($0) || $0.hasPrefix(lang) }
}