Files
simvision/simvision/Models/TVShow.swift
Michael Simard 872354b834 Initial commit: SimVision tvOS streaming app
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>
2026-01-21 22:12:08 -06:00

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