From 71acdefd0ba62ccea3ee14773ae0983fea3424d6 Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Fri, 4 Apr 2025 17:42:47 +0200 Subject: [PATCH] MULTI SERVER SUPPORT!!!!!!!!!!!!!!!!!!!!! --- .../Utils/JSLoader/JSController-Streams.swift | 251 ++++++++---------- Sora/Views/MediaInfoView/MediaInfoView.swift | 71 ++++- 2 files changed, 165 insertions(+), 157 deletions(-) diff --git a/Sora/Utils/JSLoader/JSController-Streams.swift b/Sora/Utils/JSLoader/JSController-Streams.swift index e34e2a2..85615de 100644 --- a/Sora/Utils/JSLoader/JSController-Streams.swift +++ b/Sora/Utils/JSLoader/JSController-Streams.swift @@ -33,56 +33,45 @@ extension JSController { Logger.shared.log(html, type: "HTMLStrings") if let parseFunction = self.context.objectForKeyedSubscript("extractStreamUrl"), let resultString = parseFunction.call(withArguments: [html]).toString() { - if softsub { - if let data = resultString.data(using: .utf8), - let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { - let isMultiStream = module.metadata.multiStream ?? false - let isMultiSubs = module.metadata.multiSubs ?? false - - var streamUrls: [String]? - if isMultiStream, let streamsArray = json["streams"] as? [String] { - streamUrls = streamsArray - Logger.shared.log("Found \(streamsArray.count) streams", type: "Stream") - } else if let streamUrl = json["stream"] as? String { - streamUrls = [streamUrl] - Logger.shared.log("Found single stream", type: "Stream") + if let data = resultString.data(using: .utf8) { + do { + if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { + var streamUrls: [String]? = nil + var subtitleUrls: [String]? = nil + + if let streamsArray = json["streams"] as? [String] { + streamUrls = streamsArray + Logger.shared.log("Found \(streamsArray.count) streams", type: "Stream") + } else if let streamUrl = json["stream"] as? String { + streamUrls = [streamUrl] + Logger.shared.log("Found single stream", type: "Stream") + } + + if let subsArray = json["subtitles"] as? [String] { + subtitleUrls = subsArray + Logger.shared.log("Found \(subsArray.count) subtitle tracks", type: "Stream") + } else if let subtitleUrl = json["subtitles"] as? String { + subtitleUrls = [subtitleUrl] + Logger.shared.log("Found single subtitle track", type: "Stream") + } + + Logger.shared.log("Starting stream with \(streamUrls?.count ?? 0) sources and \(subtitleUrls?.count ?? 0) subtitles", type: "Stream") + DispatchQueue.main.async { + completion((streamUrls, subtitleUrls)) + } + return } - var subtitleUrls: [String]? - if isMultiSubs, let subsArray = json["subtitles"] as? [String] { - subtitleUrls = subsArray - Logger.shared.log("Found \(subsArray.count) subtitle tracks", type: "Stream") - } else if let subtitleUrl = json["subtitles"] as? String { - subtitleUrls = [subtitleUrl] - Logger.shared.log("Found single subtitle track", type: "Stream") - } - - Logger.shared.log("Starting stream with \(streamUrls?.count ?? 0) sources and \(subtitleUrls?.count ?? 0) subtitles", type: "Stream") - DispatchQueue.main.async { - completion((streamUrls, subtitleUrls)) - } - } else { - Logger.shared.log("Failed to parse softsub JSON", type: "Error") - DispatchQueue.main.async { completion((nil, nil)) } - } - } else { - let moduleMetadata = self.context.objectForKeyedSubscript("module")?.objectForKeyedSubscript("metadata") - let isMultiStream = moduleMetadata?.objectForKeyedSubscript("multiStream")?.toBool() == true - - if isMultiStream { - if let data = resultString.data(using: .utf8), - let streamsArray = try? JSONSerialization.jsonObject(with: data, options: []) as? [String] { + if let streamsArray = try? JSONSerialization.jsonObject(with: data, options: []) as? [String] { Logger.shared.log("Starting multi-stream with \(streamsArray.count) sources", type: "Stream") DispatchQueue.main.async { completion((streamsArray, nil)) } - } else { - Logger.shared.log("Failed to parse multi-stream JSON array", type: "Error") - DispatchQueue.main.async { completion((nil, nil)) } + return } - } else { - Logger.shared.log("Starting stream from: \(resultString)", type: "Stream") - DispatchQueue.main.async { completion(([resultString], nil)) } } } + + Logger.shared.log("Starting stream from: \(resultString)", type: "Stream") + DispatchQueue.main.async { completion(([resultString], nil)) } } else { Logger.shared.log("Failed to extract stream URL", type: "Error") DispatchQueue.main.async { completion((nil, nil)) } @@ -111,65 +100,51 @@ extension JSController { } let thenBlock: @convention(block) (JSValue) -> Void = { [weak self] result in - guard let self = self else { return } + guard self != nil else { return } - if softsub { - if let jsonString = result.toString(), - let data = jsonString.data(using: .utf8), - let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { - let isMultiStream = module.metadata.multiStream ?? false - let isMultiSubs = module.metadata.multiSubs ?? false - - var streamUrls: [String]? - if isMultiStream, let streamsArray = json["streams"] as? [String] { - streamUrls = streamsArray - Logger.shared.log("Found \(streamsArray.count) streams", type: "Stream") - } else if let streamUrl = json["stream"] as? String { - streamUrls = [streamUrl] - Logger.shared.log("Found single stream", type: "Stream") + if let jsonString = result.toString(), + let data = jsonString.data(using: .utf8) { + do { + if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { + var streamUrls: [String]? = nil + var subtitleUrls: [String]? = nil + + if let streamsArray = json["streams"] as? [String] { + streamUrls = streamsArray + Logger.shared.log("Found \(streamsArray.count) streams", type: "Stream") + } else if let streamUrl = json["stream"] as? String { + streamUrls = [streamUrl] + Logger.shared.log("Found single stream", type: "Stream") + } + + if let subsArray = json["subtitles"] as? [String] { + subtitleUrls = subsArray + Logger.shared.log("Found \(subsArray.count) subtitle tracks", type: "Stream") + } else if let subtitleUrl = json["subtitles"] as? String { + subtitleUrls = [subtitleUrl] + Logger.shared.log("Found single subtitle track", type: "Stream") + } + + Logger.shared.log("Starting stream with \(streamUrls?.count ?? 0) sources and \(subtitleUrls?.count ?? 0) subtitles", type: "Stream") + DispatchQueue.main.async { + completion((streamUrls, subtitleUrls)) + } + return } - var subtitleUrls: [String]? - if isMultiSubs, let subsArray = json["subtitles"] as? [String] { - subtitleUrls = subsArray - Logger.shared.log("Found \(subsArray.count) subtitle tracks", type: "Stream") - } else if let subtitleUrl = json["subtitles"] as? String { - subtitleUrls = [subtitleUrl] - Logger.shared.log("Found single subtitle track", type: "Stream") - } - - Logger.shared.log("Starting stream with \(streamUrls?.count ?? 0) sources and \(subtitleUrls?.count ?? 0) subtitles", type: "Stream") - DispatchQueue.main.async { - completion((streamUrls, subtitleUrls)) - } - } else { - Logger.shared.log("Failed to parse softsub JSON in JS", type: "Error") - DispatchQueue.main.async { - completion((nil, nil)) - } - } - } else { - let moduleMetadata = self.context.objectForKeyedSubscript("module")?.objectForKeyedSubscript("metadata") - let isMultiStream = moduleMetadata?.objectForKeyedSubscript("multiStream")?.toBool() == true - - if isMultiStream { - if let jsonString = result.toString(), - let data = jsonString.data(using: .utf8), - let streamsArray = try? JSONSerialization.jsonObject(with: data, options: []) as? [String] { + if let streamsArray = try? JSONSerialization.jsonObject(with: data, options: []) as? [String] { Logger.shared.log("Starting multi-stream with \(streamsArray.count) sources", type: "Stream") DispatchQueue.main.async { completion((streamsArray, nil)) } - } else { - Logger.shared.log("Failed to parse multi-stream JSON array", type: "Error") - DispatchQueue.main.async { completion((nil, nil)) } - } - } else { - let streamUrl = result.toString() - Logger.shared.log("Starting stream from: \(streamUrl ?? "nil")", type: "Stream") - DispatchQueue.main.async { - completion((streamUrl != nil ? [streamUrl!] : nil, nil)) + return } } } + + let streamUrl = result.toString() + Logger.shared.log("Starting stream from: \(streamUrl ?? "nil")", type: "Stream") + DispatchQueue.main.async { + completion((streamUrl != nil ? [streamUrl!] : nil, nil)) + } } let catchBlock: @convention(block) (JSValue) -> Void = { error in @@ -224,65 +199,51 @@ extension JSController { } let thenBlock: @convention(block) (JSValue) -> Void = { [weak self] result in - guard let self = self else { return } + guard self != nil else { return } - if softsub { - if let jsonString = result.toString(), - let data = jsonString.data(using: .utf8), - let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { - let isMultiStream = module.metadata.multiStream ?? false - let isMultiSubs = module.metadata.multiSubs ?? false - - var streamUrls: [String]? - if isMultiStream, let streamsArray = json["streams"] as? [String] { - streamUrls = streamsArray - Logger.shared.log("Found \(streamsArray.count) streams", type: "Stream") - } else if let streamUrl = json["stream"] as? String { - streamUrls = [streamUrl] - Logger.shared.log("Found single stream", type: "Stream") + if let jsonString = result.toString(), + let data = jsonString.data(using: .utf8) { + do { + if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { + var streamUrls: [String]? = nil + var subtitleUrls: [String]? = nil + + if let streamsArray = json["streams"] as? [String] { + streamUrls = streamsArray + Logger.shared.log("Found \(streamsArray.count) streams", type: "Stream") + } else if let streamUrl = json["stream"] as? String { + streamUrls = [streamUrl] + Logger.shared.log("Found single stream", type: "Stream") + } + + if let subsArray = json["subtitles"] as? [String] { + subtitleUrls = subsArray + Logger.shared.log("Found \(subsArray.count) subtitle tracks", type: "Stream") + } else if let subtitleUrl = json["subtitles"] as? String { + subtitleUrls = [subtitleUrl] + Logger.shared.log("Found single subtitle track", type: "Stream") + } + + Logger.shared.log("Starting stream with \(streamUrls?.count ?? 0) sources and \(subtitleUrls?.count ?? 0) subtitles", type: "Stream") + DispatchQueue.main.async { + completion((streamUrls, subtitleUrls)) + } + return } - var subtitleUrls: [String]? - if isMultiSubs, let subsArray = json["subtitles"] as? [String] { - subtitleUrls = subsArray - Logger.shared.log("Found \(subsArray.count) subtitle tracks", type: "Stream") - } else if let subtitleUrl = json["subtitles"] as? String { - subtitleUrls = [subtitleUrl] - Logger.shared.log("Found single subtitle track", type: "Stream") - } - - Logger.shared.log("Starting stream with \(streamUrls?.count ?? 0) sources and \(subtitleUrls?.count ?? 0) subtitles", type: "Stream") - DispatchQueue.main.async { - completion((streamUrls, subtitleUrls)) - } - } else { - Logger.shared.log("Failed to parse softsub JSON in JSSecond", type: "Error") - DispatchQueue.main.async { - completion((nil, nil)) - } - } - } else { - let moduleMetadata = self.context.objectForKeyedSubscript("module")?.objectForKeyedSubscript("metadata") - let isMultiStream = moduleMetadata?.objectForKeyedSubscript("multiStream")?.toBool() == true - - if isMultiStream { - if let jsonString = result.toString(), - let data = jsonString.data(using: .utf8), - let streamsArray = try? JSONSerialization.jsonObject(with: data, options: []) as? [String] { + if let streamsArray = try? JSONSerialization.jsonObject(with: data, options: []) as? [String] { Logger.shared.log("Starting multi-stream with \(streamsArray.count) sources", type: "Stream") DispatchQueue.main.async { completion((streamsArray, nil)) } - } else { - Logger.shared.log("Failed to parse multi-stream JSON array", type: "Error") - DispatchQueue.main.async { completion((nil, nil)) } - } - } else { - let streamUrl = result.toString() - Logger.shared.log("Starting stream from: \(streamUrl ?? "nil")", type: "Stream") - DispatchQueue.main.async { - completion((streamUrl != nil ? [streamUrl!] : nil, nil)) + return } } } + + let streamUrl = result.toString() + Logger.shared.log("Starting stream from: \(streamUrl ?? "nil")", type: "Stream") + DispatchQueue.main.async { + completion((streamUrl != nil ? [streamUrl!] : nil, nil)) + } } let catchBlock: @convention(block) (JSValue) -> Void = { error in diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 6c56700..8af8e5f 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -259,8 +259,8 @@ struct MediaInfoView: View { Logger.shared.log("Marked episodes watched within season \(selectedSeason + 1) of \"\(title)\".", type: "General") } ) - .id(refreshTrigger) - .disabled(isFetchingEpisode) + .id(refreshTrigger) + .disabled(isFetchingEpisode) } } else { Text("No episodes available") @@ -299,8 +299,8 @@ struct MediaInfoView: View { Logger.shared.log("Marked \(ep.number - 1) episodes watched within series \"\(title)\".", type: "General") } ) - .id(refreshTrigger) - .disabled(isFetchingEpisode) + .id(refreshTrigger) + .disabled(isFetchingEpisode) } } } @@ -448,7 +448,7 @@ struct MediaInfoView: View { groups.append(currentGroup) return groups } - + func fetchDetails() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { @@ -500,9 +500,12 @@ struct MediaInfoView: View { if module.metadata.softsub == true { if module.metadata.asyncJS == true { jsController.fetchStreamUrlJS(episodeUrl: href, softsub: true, module: module) { result in - // Use first stream from array if let streams = result.streams, !streams.isEmpty { - self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first) + if streams.count > 1 { + self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) + } else { + self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first) + } } else { self.handleStreamFailure(error: nil) } @@ -513,7 +516,11 @@ struct MediaInfoView: View { } else if module.metadata.streamAsyncJS == true { jsController.fetchStreamUrlJSSecond(episodeUrl: href, softsub: true, module: module) { result in if let streams = result.streams, !streams.isEmpty { - self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first) + if streams.count > 1 { + self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) + } else { + self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first) + } } else { self.handleStreamFailure(error: nil) } @@ -524,7 +531,11 @@ struct MediaInfoView: View { } else { jsController.fetchStreamUrl(episodeUrl: href, softsub: true, module: module) { result in if let streams = result.streams, !streams.isEmpty { - self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first) + if streams.count > 1 { + self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) + } else { + self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first) + } } else { self.handleStreamFailure(error: nil) } @@ -537,7 +548,11 @@ struct MediaInfoView: View { if module.metadata.asyncJS == true { jsController.fetchStreamUrlJS(episodeUrl: href, module: module) { result in if let streams = result.streams, !streams.isEmpty { - self.playStream(url: streams[0], fullURL: href) + if streams.count > 1 { + self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) + } else { + self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first) + } } else { self.handleStreamFailure(error: nil) } @@ -548,7 +563,11 @@ struct MediaInfoView: View { } else if module.metadata.streamAsyncJS == true { jsController.fetchStreamUrlJSSecond(episodeUrl: href, module: module) { result in if let streams = result.streams, !streams.isEmpty { - self.playStream(url: streams[0], fullURL: href) + if streams.count > 1 { + self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) + } else { + self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first) + } } else { self.handleStreamFailure(error: nil) } @@ -559,7 +578,11 @@ struct MediaInfoView: View { } else { jsController.fetchStreamUrl(episodeUrl: href, module: module) { result in if let streams = result.streams, !streams.isEmpty { - self.playStream(url: streams[0], fullURL: href) + if streams.count > 1 { + self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) + } else { + self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first) + } } else { self.handleStreamFailure(error: nil) } @@ -590,6 +613,30 @@ struct MediaInfoView: View { self.isLoading = false } + func showStreamSelectionAlert(streams: [String], fullURL: String, subtitles: String? = nil) { + DispatchQueue.main.async { + let alert = UIAlertController(title: "Select Server", message: "Choose a server to play from", preferredStyle: .actionSheet) + + for (index, stream) in streams.enumerated() { + let quality = "Stream \(index + 1)" + alert.addAction(UIAlertAction(title: quality, style: .default) { _ in + self.playStream(url: stream, fullURL: fullURL, subtitles: subtitles) + }) + } + + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootVC = windowScene.windows.first?.rootViewController { + findTopViewController.findViewController(rootVC).present(alert, animated: true) + } + + DispatchQueue.main.async { + self.isFetchingEpisode = false + } + } + } + func playStream(url: String, fullURL: String, subtitles: String? = nil) { DispatchQueue.main.async { let externalPlayer = UserDefaults.standard.string(forKey: "externalPlayer") ?? "Sora"