diff --git a/Sora.xcodeproj/project.pbxproj b/Sora.xcodeproj/project.pbxproj index 3a71482..d2e0656 100644 --- a/Sora.xcodeproj/project.pbxproj +++ b/Sora.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 133D7C972D2BE2AF0075467E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 133D7C962D2BE2AF0075467E /* Kingfisher */; }; 138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */; }; 138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */; }; + 13DC0C462D302C7500D0F966 /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -43,6 +44,7 @@ 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeCell.swift; sourceTree = ""; }; 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = ""; }; 13DC0C412D2EC9BA00D0F966 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -76,6 +78,7 @@ 133D7C6C2D2BE2500075467E /* Sora */ = { isa = PBXGroup; children = ( + 13DC0C442D302C6A00D0F966 /* MediaPlayer */, 13DC0C412D2EC9BA00D0F966 /* Info.plist */, 133D7C852D2BE2640075467E /* Utils */, 133D7C7B2D2BE2630075467E /* Views */, @@ -168,6 +171,14 @@ path = EpisodeCell; sourceTree = ""; }; + 13DC0C442D302C6A00D0F966 /* MediaPlayer */ = { + isa = PBXGroup; + children = ( + 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */, + ); + path = MediaPlayer; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -244,6 +255,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 13DC0C462D302C7500D0F966 /* VideoPlayer.swift in Sources */, 133D7C902D2BE2640075467E /* SettingsView.swift in Sources */, 133D7C932D2BE2640075467E /* Modules.swift in Sources */, 133D7C702D2BE2500075467E /* ContentView.swift in Sources */, diff --git a/Sora/MediaPlayer/VideoPlayer.swift b/Sora/MediaPlayer/VideoPlayer.swift new file mode 100644 index 0000000..bc85c25 --- /dev/null +++ b/Sora/MediaPlayer/VideoPlayer.swift @@ -0,0 +1,98 @@ +// +// VideoPlayer.swift +// Sora +// +// Created by Francesco on 09/01/25. +// + +import UIKit +import AVKit + +class VideoPlayerViewController: UIViewController { + let module: ScrapingModule + + var player: AVPlayer? + var playerViewController: AVPlayerViewController? + var timeObserverToken: Any? + var streamUrl: String? + var fullUrl: String = "" + + init(module: ScrapingModule) { + self.module = module + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + guard let streamUrl = streamUrl, let url = URL(string: streamUrl) else { + return + } + + var request = URLRequest(url: url) + if streamUrl.contains("ascdn") { + request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer") + } + + let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]]) + let playerItem = AVPlayerItem(asset: asset) + + player = AVPlayer(playerItem: playerItem) + playerViewController = AVPlayerViewController() + playerViewController?.player = player + addPeriodicTimeObserver(fullURL: fullUrl) + + if let playerViewController = playerViewController { + playerViewController.view.frame = self.view.frame + self.view.addSubview(playerViewController.view) + self.addChild(playerViewController) + playerViewController.didMove(toParent: self) + } + + let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(fullUrl)") + if lastPlayedTime > 0 { + let seekTime = CMTime(seconds: lastPlayedTime, preferredTimescale: 1) + self.player?.seek(to: seekTime) { _ in + self.player?.play() + } + } else { + self.player?.play() + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + player?.play() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + player?.pause() + if let timeObserverToken = timeObserverToken { + player?.removeTimeObserver(timeObserverToken) + self.timeObserverToken = nil + } + } + + func addPeriodicTimeObserver(fullURL: String) { + guard let player = self.player else { return } + + let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in + guard let currentItem = player.currentItem, + currentItem.duration.seconds.isFinite else { + return + } + + let currentTime = time.seconds + let duration = currentItem.duration.seconds + + UserDefaults.standard.set(currentTime, forKey: "lastPlayedTime_\(fullURL)") + UserDefaults.standard.set(duration, forKey: "totalTime_\(fullURL)") + } + } +} diff --git a/Sora/Utils/Loaders/JSController.swift b/Sora/Utils/Loaders/JSController.swift index c54f029..4737f61 100644 --- a/Sora/Utils/Loaders/JSController.swift +++ b/Sora/Utils/Loaders/JSController.swift @@ -121,4 +121,37 @@ class JSController: ObservableObject { } }.resume() } + + func fetchStreamUrl(episodeUrl: String, completion: @escaping (String?) -> Void) { + guard let url = URL(string: episodeUrl) else { + completion(nil) + return + } + + URLSession.custom.dataTask(with: url) { [weak self] data, _, error in + guard let self = self else { return } + + if let error = error { + print("Network error: \(error)") + DispatchQueue.main.async { completion(nil) } + return + } + + guard let data = data, let html = String(data: data, encoding: .utf8) else { + print("Failed to decode HTML") + DispatchQueue.main.async { completion(nil) } + return + } + + if let parseFunction = self.context.objectForKeyedSubscript("extractStreamUrl"), + let streamUrl = parseFunction.call(withArguments: [html]).toString() { + DispatchQueue.main.async { + completion(streamUrl) + } + } else { + print("Failed to extract stream URL") + DispatchQueue.main.async { completion(nil) } + } + }.resume() + } } diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 861139b..17aeacb 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -165,6 +165,9 @@ struct MediaInfoView: View { let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0 EpisodeCell(episode: ep.href, episodeID: ep.number - 1, progress: progress, itemID: itemID ?? 0) + .onTapGesture { + fetchStream(href: ep.href) + } } } } @@ -212,6 +215,39 @@ struct MediaInfoView: View { } } + func fetchStream(href: String) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + Task { + do { + let jsContent = try moduleManager.getModuleContent(module) + jsController.loadScript(jsContent) + jsController.fetchStreamUrl(episodeUrl: href) { streamUrl in + if let url = streamUrl { + playStream(url: url, fullURL: url) + } + } + } catch { + print("Error loading module: \(error)") + self.isLoading = false + } + } + } + } + + func playStream(url: String, fullURL: String) { + DispatchQueue.main.async { + let videoPlayerViewController = VideoPlayerViewController(module: module) + videoPlayerViewController.streamUrl = url + videoPlayerViewController.fullUrl = fullURL + videoPlayerViewController.modalPresentationStyle = .fullScreen + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootVC = windowScene.windows.first?.rootViewController { + rootVC.present(videoPlayerViewController, animated: true, completion: nil) + } + } + } + private func openSafariViewController(with urlString: String) { guard let url = URL(string: urlString) else { print("Unable to open the webpage")