Features: - VOD library with movie grouping and version detection - TV show library with season/episode organization - TMDB integration for trending shows and recently aired episodes - Recent releases section with TMDB release date sorting - Watch history tracking with continue watching - Playlist caching (12-hour TTL) for offline support - M3U playlist parsing with XStream API support - Authentication with credential storage Technical: - SwiftUI for tvOS - Actor-based services for thread safety - Persistent caching for playlists, TMDB data, and watch history - KSPlayer integration for video playback Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
91 lines
3.4 KiB
Swift
91 lines
3.4 KiB
Swift
import Foundation
|
|
|
|
// MARK: - Cached Regex Patterns for TV Show Parsing
|
|
private enum TVShowRegex {
|
|
static let seasonPattern: NSRegularExpression? = try? NSRegularExpression(
|
|
pattern: "S(\\d+)", options: .caseInsensitive)
|
|
static let episodePattern: NSRegularExpression? = try? NSRegularExpression(
|
|
pattern: "E(\\d+)", options: .caseInsensitive)
|
|
static let showNamePattern: NSRegularExpression? = try? NSRegularExpression(
|
|
pattern: "\\s+S\\d+.*$", options: .caseInsensitive)
|
|
}
|
|
|
|
struct TVShow: Identifiable, Hashable, Sendable {
|
|
let id: String
|
|
let name: String
|
|
let iconURL: String?
|
|
let categoryID: String?
|
|
let seasons: [Season]
|
|
|
|
init(name: String, episodes: [VODItem]) {
|
|
self.name = name
|
|
self.id = name // Use show name as unique identifier
|
|
|
|
// Use the first episode's metadata for show-level info
|
|
if let firstEpisode = episodes.first {
|
|
self.iconURL = firstEpisode.iconURL
|
|
self.categoryID = firstEpisode.categoryID
|
|
} else {
|
|
self.iconURL = nil
|
|
self.categoryID = nil
|
|
}
|
|
|
|
// Group episodes by season
|
|
var seasonDict: [Int: [VODItem]] = [:]
|
|
for episode in episodes {
|
|
if let seasonNumber = Self.extractSeasonNumber(from: episode.name) {
|
|
seasonDict[seasonNumber, default: []].append(episode)
|
|
}
|
|
}
|
|
|
|
// Convert to Season objects and sort by season number
|
|
self.seasons = seasonDict.map { seasonNumber, episodes in
|
|
Season(number: seasonNumber, episodes: episodes.sorted {
|
|
Self.extractEpisodeNumber(from: $0.name) ?? 0 < Self.extractEpisodeNumber(from: $1.name) ?? 0
|
|
})
|
|
}.sorted { $0.number < $1.number }
|
|
}
|
|
|
|
// Extract season number from episode name (e.g., "Breaking Bad S01 E01" -> 1)
|
|
static func extractSeasonNumber(from name: String) -> Int? {
|
|
guard let regex = TVShowRegex.seasonPattern,
|
|
let match = regex.firstMatch(in: name, options: [], range: NSRange(name.startIndex..., in: name)),
|
|
let range = Range(match.range(at: 1), in: name) else {
|
|
return nil
|
|
}
|
|
return Int(name[range])
|
|
}
|
|
|
|
// Extract episode number from episode name (e.g., "Breaking Bad S01 E01" -> 1)
|
|
static func extractEpisodeNumber(from name: String) -> Int? {
|
|
guard let regex = TVShowRegex.episodePattern,
|
|
let match = regex.firstMatch(in: name, options: [], range: NSRange(name.startIndex..., in: name)),
|
|
let range = Range(match.range(at: 1), in: name) else {
|
|
return nil
|
|
}
|
|
return Int(name[range])
|
|
}
|
|
|
|
// Extract show name from episode name (e.g., "Breaking Bad S01 E01" -> "Breaking Bad")
|
|
static func extractShowName(from name: String) -> String {
|
|
guard let regex = TVShowRegex.showNamePattern else {
|
|
return name
|
|
}
|
|
let range = NSRange(name.startIndex..., in: name)
|
|
let cleanName = regex.stringByReplacingMatches(in: name, options: [], range: range, withTemplate: "")
|
|
return cleanName.trimmingCharacters(in: .whitespaces)
|
|
}
|
|
}
|
|
|
|
struct Season: Identifiable, Hashable, Sendable {
|
|
let id: String
|
|
let number: Int
|
|
let episodes: [VODItem]
|
|
|
|
init(number: Int, episodes: [VODItem]) {
|
|
self.number = number
|
|
self.id = "season-\(number)"
|
|
self.episodes = episodes
|
|
}
|
|
}
|