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>
This commit is contained in:
2026-01-21 22:12:08 -06:00
commit 872354b834
283 changed files with 338296 additions and 0 deletions

View File

@@ -0,0 +1,253 @@
//
// TracyApp.swift
// Shared
//
// Created by kintan on 2021/5/3.
//
import AVFoundation
import AVKit
import KSPlayer
import SwiftUI
import UserNotifications
@main
struct TracyApp: App {
#if os(macOS)
@NSApplicationDelegateAdaptor
#else
@UIApplicationDelegateAdaptor
#endif
private var appDelegate: AppDelegate
private let appModel = APPModel()
init() {
let arguments = ProcessInfo.processInfo.arguments.dropFirst()
var dropNextArg = false
var playerArgs = [String]()
var filenames = [String]()
KSLog("launch arguments \(arguments)")
for argument in arguments {
if dropNextArg {
dropNextArg = false
continue
}
if argument.starts(with: "--") {
playerArgs.append(argument)
} else if argument.starts(with: "-") {
dropNextArg = true
} else {
filenames.append(argument)
}
}
if let urlString = filenames.first {
appModel.open(url: URL(fileURLWithPath: urlString))
}
}
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appModel)
.environment(\.managedObjectContext, PersistenceController.shared.viewContext)
#if !os(tvOS)
.handlesExternalEvents(preferring: Set(arrayLiteral: "pause"), allowing: Set(arrayLiteral: "*"))
#endif
}
#if !os(tvOS)
// .handlesExternalEvents(matching: Set(arrayLiteral: "*"))
.commands {
CommandGroup(before: .newItem) {
Button("Open") {
appModel.openFileImport = true
}
.keyboardShortcut("o")
}
CommandGroup(before: .newItem) {
Button("Open URL") {
appModel.openURLImport = true
}
.keyboardShortcut("o", modifiers: [.command, .shift])
}
}
#endif
#if os(macOS)
.defaultSize(width: 1120, height: 630)
.defaultPosition(.center)
#endif
#if !os(tvOS)
WindowGroup("player", for: URL.self) { $url in
if let url {
KSVideoPlayerView(url: url)
}
}
#if os(macOS)
.defaultPosition(.center)
#endif
#endif
#if !os(tvOS)
WindowGroup("player", for: MovieModel.self) { $model in
if let model {
KSVideoPlayerView(model: model)
}
}
#if os(macOS)
.defaultPosition(.center)
#endif
#endif
#if os(macOS)
Settings {
TabBarItem.Setting.destination(appModel: appModel)
}
// MenuBarExtra {
// MenuBar()
// } label: {
// Image(systemName: "film.fill")
// }
// .menuBarExtraStyle(.menu)
#endif
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
#if os(macOS)
func applicationDidFinishLaunching(_: Notification) {
// requestNotification()
}
#else
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
// requestNotification()
true
}
#endif
func application(_: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenString = deviceToken.reduce("") { $0 + String(format: "%02x", $1) }
print("Device push notification token - \(tokenString)")
}
func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Failed to register for remote notification. Error \(error)")
}
private func requestNotification() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { allowed, error in
if allowed {
// register for remote push notification
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
print("Push notification allowed by user")
} else {
print("Error while requesting push notification permission. Error \(String(describing: error))")
}
}
}
}
class APPModel: ObservableObject {
@Published
var openURL: URL?
@Published
var openPlayModel: MovieModel?
@Published
var tabSelected: TabBarItem = .Home
@Published
var path = NavigationPath()
@Published
var openFileImport = false
@Published
var openURLImport = false
@Published
var hiddenTitleBar = false
@AppStorage("activeM3UURL")
private var activeM3UURL: URL?
@Published
var activeM3UModel: M3UModel? = nil {
didSet {
if let activeM3UModel, activeM3UModel != oldValue {
activeM3UURL = activeM3UModel.m3uURL
Task { @MainActor in
_ = try? await activeM3UModel.parsePlaylist()
// m3u
if activeM3UModel == self.activeM3UModel {
self.activeM3UModel = activeM3UModel
}
}
}
}
}
init() {
#if DEBUG
// KSOptions.logLevel = .debug
#else
var fileHandle = FileHandle.standardOutput
if let logURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("log.txt") {
if !FileManager.default.fileExists(atPath: logURL.path) {
FileManager.default.createFile(atPath: logURL.path, contents: nil)
}
if let handle = try? FileHandle(forWritingTo: logURL) {
fileHandle = handle
_ = try? fileHandle.seekToEnd()
}
}
KSOptions.logger = FileLog(fileHandle: fileHandle)
#endif
// KSOptions.firstPlayerType = KSMEPlayer.self
KSOptions.secondPlayerType = KSMEPlayer.self
_ = Defaults.shared
KSOptions.subtitleDataSouces = [DirectorySubtitleDataSouce(), ShooterSubtitleDataSouce(), AssrtSubtitleDataSouce(token: "5IzWrb2J099vmA96ECQXwdRSe9xdoBUv"), OpenSubtitleDataSouce(apiKey: "0D0gt8nV6SFHVVejdxAMpvOT0wByfKE5")]
if let activeM3UURL {
addM3U(url: activeM3UURL)
}
}
func addM3U(url: URL, name: String? = nil) {
let request = M3UModel.fetchRequest()
request.predicate = NSPredicate(format: "m3uURL == %@", url.description)
let context = PersistenceController.shared.viewContext
context.perform {
self.activeM3UModel = try? context.fetch(request).first ?? M3UModel(context: context, url: url, name: name)
}
}
func open(url: URL) {
if url.isPlaylist {
addM3U(url: url)
} else {
#if os(macOS)
openURL = url
#else
path.append(url)
#endif
}
}
func content(model: MovieModel) -> some View {
#if os(macOS)
Button {
self.openPlayModel = model
} label: {
MoiveView(model: model)
}
.buttonStyle(.borderless)
#else
NavigationLink(value: model) {
MoiveView(model: model)
}
#if targetEnvironment(macCatalyst)
.buttonStyle(.borderless)
#else
.buttonStyle(.automatic)
#endif
#endif
}
}
struct KSVideoPlayerView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(APPModel())
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}