mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +00:00
made it use an Array if needed
Some checks failed
Build and Release IPA / Build IPA (push) Has been cancelled
Some checks failed
Build and Release IPA / Build IPA (push) Has been cancelled
This commit is contained in:
parent
ee81815afa
commit
6243d456ca
7 changed files with 650 additions and 492 deletions
185
Sora/Utils/JSLoader/JSController-Details.swift
Normal file
185
Sora/Utils/JSLoader/JSController-Details.swift
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
//
|
||||
// JSControllerDetails.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 30/03/25.
|
||||
//
|
||||
|
||||
import JavaScriptCore
|
||||
|
||||
extension JSController {
|
||||
|
||||
func fetchDetails(url: String, completion: @escaping ([MediaItem], [EpisodeLink]) -> Void) {
|
||||
guard let url = URL(string: url) else {
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
||||
URLSession.custom.dataTask(with: url) { [weak self] data, _, error in
|
||||
guard let self = self else { return }
|
||||
|
||||
if let error = error {
|
||||
Logger.shared.log("Network error: \(error)",type: "Error")
|
||||
DispatchQueue.main.async { completion([], []) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let html = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to decode HTML",type: "Error")
|
||||
DispatchQueue.main.async { completion([], []) }
|
||||
return
|
||||
}
|
||||
|
||||
var resultItems: [MediaItem] = []
|
||||
var episodeLinks: [EpisodeLink] = []
|
||||
|
||||
Logger.shared.log(html,type: "HTMLStrings")
|
||||
if let parseFunction = self.context.objectForKeyedSubscript("extractDetails"),
|
||||
let results = parseFunction.call(withArguments: [html]).toArray() as? [[String: String]] {
|
||||
resultItems = results.map { item in
|
||||
MediaItem(
|
||||
description: item["description"] ?? "",
|
||||
aliases: item["aliases"] ?? "",
|
||||
airdate: item["airdate"] ?? ""
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse results",type: "Error")
|
||||
}
|
||||
|
||||
if let fetchEpisodesFunction = self.context.objectForKeyedSubscript("extractEpisodes"),
|
||||
let episodesResult = fetchEpisodesFunction.call(withArguments: [html]).toArray() as? [[String: String]] {
|
||||
for episodeData in episodesResult {
|
||||
if let num = episodeData["number"], let link = episodeData["href"], let number = Int(num) {
|
||||
episodeLinks.append(EpisodeLink(number: number, href: link))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(resultItems, episodeLinks)
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func fetchDetailsJS(url: String, completion: @escaping ([MediaItem], [EpisodeLink]) -> Void) {
|
||||
guard let url = URL(string: url) else {
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
||||
if let exception = context.exception {
|
||||
Logger.shared.log("JavaScript exception: \(exception)",type: "Error")
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
||||
guard let extractDetailsFunction = context.objectForKeyedSubscript("extractDetails") else {
|
||||
Logger.shared.log("No JavaScript function extractDetails found",type: "Error")
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
||||
guard let extractEpisodesFunction = context.objectForKeyedSubscript("extractEpisodes") else {
|
||||
Logger.shared.log("No JavaScript function extractEpisodes found",type: "Error")
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
||||
var resultItems: [MediaItem] = []
|
||||
var episodeLinks: [EpisodeLink] = []
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
dispatchGroup.enter()
|
||||
let promiseValueDetails = extractDetailsFunction.call(withArguments: [url.absoluteString])
|
||||
guard let promiseDetails = promiseValueDetails else {
|
||||
Logger.shared.log("extractDetails did not return a Promise",type: "Error")
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
||||
let thenBlockDetails: @convention(block) (JSValue) -> Void = { result in
|
||||
Logger.shared.log(result.toString(),type: "Debug")
|
||||
if let jsonOfDetails = result.toString(),
|
||||
let dataDetails = jsonOfDetails.data(using: .utf8) {
|
||||
do {
|
||||
if let array = try JSONSerialization.jsonObject(with: dataDetails, options: []) as? [[String: Any]] {
|
||||
resultItems = array.map { item -> MediaItem in
|
||||
MediaItem(
|
||||
description: item["description"] as? String ?? "",
|
||||
aliases: item["aliases"] as? String ?? "",
|
||||
airdate: item["airdate"] as? String ?? ""
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse JSON of extractDetails",type: "Error")
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("JSON parsing error of extract details: \(error)",type: "Error")
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Result is not a string of extractDetails",type: "Error")
|
||||
}
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
let catchBlockDetails: @convention(block) (JSValue) -> Void = { error in
|
||||
Logger.shared.log("Promise rejected of extractDetails: \(String(describing: error.toString()))",type: "Error")
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
let thenFunctionDetails = JSValue(object: thenBlockDetails, in: context)
|
||||
let catchFunctionDetails = JSValue(object: catchBlockDetails, in: context)
|
||||
|
||||
promiseDetails.invokeMethod("then", withArguments: [thenFunctionDetails as Any])
|
||||
promiseDetails.invokeMethod("catch", withArguments: [catchFunctionDetails as Any])
|
||||
|
||||
dispatchGroup.enter()
|
||||
let promiseValueEpisodes = extractEpisodesFunction.call(withArguments: [url.absoluteString])
|
||||
guard let promiseEpisodes = promiseValueEpisodes else {
|
||||
Logger.shared.log("extractEpisodes did not return a Promise",type: "Error")
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
||||
let thenBlockEpisodes: @convention(block) (JSValue) -> Void = { result in
|
||||
Logger.shared.log(result.toString(),type: "Debug")
|
||||
if let jsonOfEpisodes = result.toString(),
|
||||
let dataEpisodes = jsonOfEpisodes.data(using: .utf8) {
|
||||
do {
|
||||
if let array = try JSONSerialization.jsonObject(with: dataEpisodes, options: []) as? [[String: Any]] {
|
||||
episodeLinks = array.map { item -> EpisodeLink in
|
||||
EpisodeLink(
|
||||
number: item["number"] as? Int ?? 0,
|
||||
href: item["href"] as? String ?? ""
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse JSON of extractEpisodes",type: "Error")
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("JSON parsing error of extractEpisodes: \(error)",type: "Error")
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Result is not a string of extractEpisodes",type: "Error")
|
||||
}
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
let catchBlockEpisodes: @convention(block) (JSValue) -> Void = { error in
|
||||
Logger.shared.log("Promise rejected of extractEpisodes: \(String(describing: error.toString()))",type: "Error")
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
let thenFunctionEpisodes = JSValue(object: thenBlockEpisodes, in: context)
|
||||
let catchFunctionEpisodes = JSValue(object: catchBlockEpisodes, in: context)
|
||||
|
||||
promiseEpisodes.invokeMethod("then", withArguments: [thenFunctionEpisodes as Any])
|
||||
promiseEpisodes.invokeMethod("catch", withArguments: [catchFunctionEpisodes as Any])
|
||||
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
completion(resultItems, episodeLinks)
|
||||
}
|
||||
}
|
||||
}
|
||||
129
Sora/Utils/JSLoader/JSController-Search.swift
Normal file
129
Sora/Utils/JSLoader/JSController-Search.swift
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
//
|
||||
// JSController-Search.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 30/03/25.
|
||||
//
|
||||
|
||||
import JavaScriptCore
|
||||
|
||||
extension JSController {
|
||||
|
||||
func fetchSearchResults(keyword: String, module: ScrapingModule, completion: @escaping ([SearchItem]) -> Void) {
|
||||
let searchUrl = module.metadata.searchBaseUrl.replacingOccurrences(of: "%s", with: keyword.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")
|
||||
|
||||
guard let url = URL(string: searchUrl) else {
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
URLSession.custom.dataTask(with: url) { [weak self] data, _, error in
|
||||
guard let self = self else { return }
|
||||
|
||||
if let error = error {
|
||||
Logger.shared.log("Network error: \(error)",type: "Error")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let html = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to decode HTML",type: "Error")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
return
|
||||
}
|
||||
|
||||
Logger.shared.log(html,type: "HTMLStrings")
|
||||
if let parseFunction = self.context.objectForKeyedSubscript("searchResults"),
|
||||
let results = parseFunction.call(withArguments: [html]).toArray() as? [[String: String]] {
|
||||
let resultItems = results.map { item in
|
||||
SearchItem(
|
||||
title: item["title"] ?? "",
|
||||
imageUrl: item["image"] ?? "",
|
||||
href: item["href"] ?? ""
|
||||
)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
completion(resultItems)
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse results",type: "Error")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func fetchJsSearchResults(keyword: String, module: ScrapingModule, completion: @escaping ([SearchItem]) -> Void) {
|
||||
if let exception = context.exception {
|
||||
Logger.shared.log("JavaScript exception: \(exception)",type: "Error")
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
guard let searchResultsFunction = context.objectForKeyedSubscript("searchResults") else {
|
||||
Logger.shared.log("No JavaScript function searchResults found",type: "Error")
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
let promiseValue = searchResultsFunction.call(withArguments: [keyword])
|
||||
guard let promise = promiseValue else {
|
||||
Logger.shared.log("searchResults did not return a Promise",type: "Error")
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
let thenBlock: @convention(block) (JSValue) -> Void = { result in
|
||||
|
||||
Logger.shared.log(result.toString(),type: "HTMLStrings")
|
||||
if let jsonString = result.toString(),
|
||||
let data = jsonString.data(using: .utf8) {
|
||||
do {
|
||||
if let array = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]] {
|
||||
let resultItems = array.compactMap { item -> SearchItem? in
|
||||
guard let title = item["title"] as? String,
|
||||
let imageUrl = item["image"] as? String,
|
||||
let href = item["href"] as? String else {
|
||||
Logger.shared.log("Missing or invalid data in search result item: \(item)", type: "Error")
|
||||
return nil
|
||||
}
|
||||
return SearchItem(title: title, imageUrl: imageUrl, href: href)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(resultItems)
|
||||
}
|
||||
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse JSON",type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion([])
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("JSON parsing error: \(error)",type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion([])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Result is not a string",type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion([])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let catchBlock: @convention(block) (JSValue) -> Void = { error in
|
||||
Logger.shared.log("Promise rejected: \(String(describing: error.toString()))",type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion([])
|
||||
}
|
||||
}
|
||||
|
||||
let thenFunction = JSValue(object: thenBlock, in: context)
|
||||
let catchFunction = JSValue(object: catchBlock, in: context)
|
||||
|
||||
promise.invokeMethod("then", withArguments: [thenFunction as Any])
|
||||
promise.invokeMethod("catch", withArguments: [catchFunction as Any])
|
||||
}
|
||||
}
|
||||
307
Sora/Utils/JSLoader/JSController-Streams.swift
Normal file
307
Sora/Utils/JSLoader/JSController-Streams.swift
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
//
|
||||
// JSLoader-Streams.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 30/03/25.
|
||||
//
|
||||
|
||||
import JavaScriptCore
|
||||
|
||||
extension JSController {
|
||||
|
||||
func fetchStreamUrl(episodeUrl: String, softsub: Bool = false, completion: @escaping ((streams: [String]?, subtitles: [String]?)) -> Void) {
|
||||
guard let url = URL(string: episodeUrl) else {
|
||||
completion((nil, nil))
|
||||
return
|
||||
}
|
||||
|
||||
URLSession.custom.dataTask(with: url) { [weak self] data, _, error in
|
||||
guard let self = self else { return }
|
||||
|
||||
if let error = error {
|
||||
Logger.shared.log("Network error: \(error)", type: "Error")
|
||||
DispatchQueue.main.async { completion((nil, nil)) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let html = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to decode HTML", type: "Error")
|
||||
DispatchQueue.main.async { completion((nil, nil)) }
|
||||
return
|
||||
}
|
||||
|
||||
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 moduleMetadata = self.context.objectForKeyedSubscript("module")?.objectForKeyedSubscript("metadata")
|
||||
let isMultiStream = moduleMetadata?.objectForKeyedSubscript("multiStream")?.toBool() == true
|
||||
let isMultiSubs = moduleMetadata?.objectForKeyedSubscript("multiSubs")?.toBool() == true
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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] {
|
||||
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 {
|
||||
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)) }
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func fetchStreamUrlJS(episodeUrl: String, softsub: Bool = false, completion: @escaping ((streams: [String]?, subtitles: [String]?)) -> Void) {
|
||||
if let exception = context.exception {
|
||||
Logger.shared.log("JavaScript exception: \(exception)", type: "Error")
|
||||
completion((nil, nil))
|
||||
return
|
||||
}
|
||||
|
||||
guard let extractStreamUrlFunction = context.objectForKeyedSubscript("extractStreamUrl") else {
|
||||
Logger.shared.log("No JavaScript function extractStreamUrl found", type: "Error")
|
||||
completion((nil, nil))
|
||||
return
|
||||
}
|
||||
|
||||
let promiseValue = extractStreamUrlFunction.call(withArguments: [episodeUrl])
|
||||
guard let promise = promiseValue else {
|
||||
Logger.shared.log("extractStreamUrl did not return a Promise", type: "Error")
|
||||
completion((nil, nil))
|
||||
return
|
||||
}
|
||||
|
||||
let thenBlock: @convention(block) (JSValue) -> Void = { [weak self] result in
|
||||
guard let self = self 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 moduleMetadata = self.context.objectForKeyedSubscript("module")?.objectForKeyedSubscript("metadata")
|
||||
let isMultiStream = moduleMetadata?.objectForKeyedSubscript("multiStream")?.toBool() == true
|
||||
let isMultiSubs = moduleMetadata?.objectForKeyedSubscript("multiSubs")?.toBool() == true
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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] {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let catchBlock: @convention(block) (JSValue) -> Void = { error in
|
||||
Logger.shared.log("Promise rejected: \(String(describing: error.toString()))", type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion((nil, nil))
|
||||
}
|
||||
}
|
||||
|
||||
let thenFunction = JSValue(object: thenBlock, in: context)
|
||||
let catchFunction = JSValue(object: catchBlock, in: context)
|
||||
|
||||
promise.invokeMethod("then", withArguments: [thenFunction as Any])
|
||||
promise.invokeMethod("catch", withArguments: [catchFunction as Any])
|
||||
}
|
||||
|
||||
func fetchStreamUrlJSSecond(episodeUrl: String, softsub: Bool = false, completion: @escaping ((streams: [String]?, subtitles: [String]?)) -> Void) {
|
||||
let url = URL(string: episodeUrl)!
|
||||
let task = URLSession.custom.dataTask(with: url) { [weak self] data, response, error in
|
||||
guard let self = self else { return }
|
||||
|
||||
if let error = error {
|
||||
Logger.shared.log("URLSession error: \(error.localizedDescription)", type: "Error")
|
||||
DispatchQueue.main.async { completion((nil, nil)) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let htmlString = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to fetch HTML data", type: "Error")
|
||||
DispatchQueue.main.async { completion((nil, nil)) }
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if let exception = self.context.exception {
|
||||
Logger.shared.log("JavaScript exception: \(exception)", type: "Error")
|
||||
completion((nil, nil))
|
||||
return
|
||||
}
|
||||
|
||||
guard let extractStreamUrlFunction = self.context.objectForKeyedSubscript("extractStreamUrl") else {
|
||||
Logger.shared.log("No JavaScript function extractStreamUrl found", type: "Error")
|
||||
completion((nil, nil))
|
||||
return
|
||||
}
|
||||
|
||||
let promiseValue = extractStreamUrlFunction.call(withArguments: [htmlString])
|
||||
guard let promise = promiseValue else {
|
||||
Logger.shared.log("extractStreamUrl did not return a Promise", type: "Error")
|
||||
completion((nil, nil))
|
||||
return
|
||||
}
|
||||
|
||||
let thenBlock: @convention(block) (JSValue) -> Void = { [weak self] result in
|
||||
guard let self = self 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 moduleMetadata = self.context.objectForKeyedSubscript("module")?.objectForKeyedSubscript("metadata")
|
||||
let isMultiStream = moduleMetadata?.objectForKeyedSubscript("multiStream")?.toBool() == true
|
||||
let isMultiSubs = moduleMetadata?.objectForKeyedSubscript("multiSubs")?.toBool() == true
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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] {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let catchBlock: @convention(block) (JSValue) -> Void = { error in
|
||||
Logger.shared.log("Promise rejected: \(String(describing: error.toString()))", type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion((nil, nil))
|
||||
}
|
||||
}
|
||||
|
||||
let thenFunction = JSValue(object: thenBlock, in: self.context)
|
||||
let catchFunction = JSValue(object: catchBlock, in: self.context)
|
||||
|
||||
promise.invokeMethod("then", withArguments: [thenFunction as Any])
|
||||
promise.invokeMethod("catch", withArguments: [catchFunction as Any])
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
|
@ -8,14 +8,14 @@
|
|||
import JavaScriptCore
|
||||
|
||||
class JSController: ObservableObject {
|
||||
private var context: JSContext
|
||||
var context: JSContext
|
||||
|
||||
init() {
|
||||
self.context = JSContext()
|
||||
setupContext()
|
||||
}
|
||||
|
||||
private func setupContext() {
|
||||
func setupContext() {
|
||||
context.setupJavaScriptEnvironment()
|
||||
}
|
||||
|
||||
|
|
@ -27,482 +27,4 @@ class JSController: ObservableObject {
|
|||
Logger.shared.log("Error loading script: \(exception)", type: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
func fetchSearchResults(keyword: String, module: ScrapingModule, completion: @escaping ([SearchItem]) -> Void) {
|
||||
let searchUrl = module.metadata.searchBaseUrl.replacingOccurrences(of: "%s", with: keyword.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")
|
||||
|
||||
guard let url = URL(string: searchUrl) else {
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
URLSession.custom.dataTask(with: url) { [weak self] data, _, error in
|
||||
guard let self = self else { return }
|
||||
|
||||
if let error = error {
|
||||
Logger.shared.log("Network error: \(error)",type: "Error")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let html = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to decode HTML",type: "Error")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
return
|
||||
}
|
||||
|
||||
Logger.shared.log(html,type: "HTMLStrings")
|
||||
if let parseFunction = self.context.objectForKeyedSubscript("searchResults"),
|
||||
let results = parseFunction.call(withArguments: [html]).toArray() as? [[String: String]] {
|
||||
let resultItems = results.map { item in
|
||||
SearchItem(
|
||||
title: item["title"] ?? "",
|
||||
imageUrl: item["image"] ?? "",
|
||||
href: item["href"] ?? ""
|
||||
)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
completion(resultItems)
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse results",type: "Error")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func fetchDetails(url: String, completion: @escaping ([MediaItem], [EpisodeLink]) -> Void) {
|
||||
guard let url = URL(string: url) else {
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
||||
URLSession.custom.dataTask(with: url) { [weak self] data, _, error in
|
||||
guard let self = self else { return }
|
||||
|
||||
if let error = error {
|
||||
Logger.shared.log("Network error: \(error)",type: "Error")
|
||||
DispatchQueue.main.async { completion([], []) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let html = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to decode HTML",type: "Error")
|
||||
DispatchQueue.main.async { completion([], []) }
|
||||
return
|
||||
}
|
||||
|
||||
var resultItems: [MediaItem] = []
|
||||
var episodeLinks: [EpisodeLink] = []
|
||||
|
||||
Logger.shared.log(html,type: "HTMLStrings")
|
||||
if let parseFunction = self.context.objectForKeyedSubscript("extractDetails"),
|
||||
let results = parseFunction.call(withArguments: [html]).toArray() as? [[String: String]] {
|
||||
resultItems = results.map { item in
|
||||
MediaItem(
|
||||
description: item["description"] ?? "",
|
||||
aliases: item["aliases"] ?? "",
|
||||
airdate: item["airdate"] ?? ""
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse results",type: "Error")
|
||||
}
|
||||
|
||||
if let fetchEpisodesFunction = self.context.objectForKeyedSubscript("extractEpisodes"),
|
||||
let episodesResult = fetchEpisodesFunction.call(withArguments: [html]).toArray() as? [[String: String]] {
|
||||
for episodeData in episodesResult {
|
||||
if let num = episodeData["number"], let link = episodeData["href"], let number = Int(num) {
|
||||
episodeLinks.append(EpisodeLink(number: number, href: link))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(resultItems, episodeLinks)
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func fetchStreamUrl(episodeUrl: String, softsub: Bool = false, completion: @escaping ((stream: String?, subtitles: String?)) -> Void) {
|
||||
guard let url = URL(string: episodeUrl) else {
|
||||
completion((nil, nil))
|
||||
return
|
||||
}
|
||||
|
||||
URLSession.custom.dataTask(with: url) { [weak self] data, _, error in
|
||||
guard let self = self else { return }
|
||||
|
||||
if let error = error {
|
||||
Logger.shared.log("Network error: \(error)", type: "Error")
|
||||
DispatchQueue.main.async { completion((nil, nil)) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let html = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to decode HTML", type: "Error")
|
||||
DispatchQueue.main.async { completion((nil, nil)) }
|
||||
return
|
||||
}
|
||||
|
||||
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 streamUrl = json["stream"] as? String
|
||||
let subtitlesUrl = json["subtitles"] as? String
|
||||
Logger.shared.log("Starting stream from: \(streamUrl ?? "nil") with subtitles: \(subtitlesUrl ?? "nil")", type: "Stream")
|
||||
DispatchQueue.main.async {
|
||||
completion((streamUrl, subtitlesUrl))
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse softsub JSON", type: "Error")
|
||||
DispatchQueue.main.async { completion((nil, nil)) }
|
||||
}
|
||||
} else {
|
||||
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)) }
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func fetchJsSearchResults(keyword: String, module: ScrapingModule, completion: @escaping ([SearchItem]) -> Void) {
|
||||
if let exception = context.exception {
|
||||
Logger.shared.log("JavaScript exception: \(exception)",type: "Error")
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
guard let searchResultsFunction = context.objectForKeyedSubscript("searchResults") else {
|
||||
Logger.shared.log("No JavaScript function searchResults found",type: "Error")
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
let promiseValue = searchResultsFunction.call(withArguments: [keyword])
|
||||
guard let promise = promiseValue else {
|
||||
Logger.shared.log("searchResults did not return a Promise",type: "Error")
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
let thenBlock: @convention(block) (JSValue) -> Void = { result in
|
||||
|
||||
Logger.shared.log(result.toString(),type: "HTMLStrings")
|
||||
if let jsonString = result.toString(),
|
||||
let data = jsonString.data(using: .utf8) {
|
||||
do {
|
||||
if let array = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]] {
|
||||
let resultItems = array.compactMap { item -> SearchItem? in
|
||||
guard let title = item["title"] as? String,
|
||||
let imageUrl = item["image"] as? String,
|
||||
let href = item["href"] as? String else {
|
||||
Logger.shared.log("Missing or invalid data in search result item: \(item)", type: "Error")
|
||||
return nil
|
||||
}
|
||||
return SearchItem(title: title, imageUrl: imageUrl, href: href)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(resultItems)
|
||||
}
|
||||
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse JSON",type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion([])
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("JSON parsing error: \(error)",type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion([])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Result is not a string",type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion([])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let catchBlock: @convention(block) (JSValue) -> Void = { error in
|
||||
Logger.shared.log("Promise rejected: \(String(describing: error.toString()))",type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion([])
|
||||
}
|
||||
}
|
||||
|
||||
let thenFunction = JSValue(object: thenBlock, in: context)
|
||||
let catchFunction = JSValue(object: catchBlock, in: context)
|
||||
|
||||
promise.invokeMethod("then", withArguments: [thenFunction as Any])
|
||||
promise.invokeMethod("catch", withArguments: [catchFunction as Any])
|
||||
}
|
||||
|
||||
func fetchDetailsJS(url: String, completion: @escaping ([MediaItem], [EpisodeLink]) -> Void) {
|
||||
guard let url = URL(string: url) else {
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
||||
if let exception = context.exception {
|
||||
Logger.shared.log("JavaScript exception: \(exception)",type: "Error")
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
||||
guard let extractDetailsFunction = context.objectForKeyedSubscript("extractDetails") else {
|
||||
Logger.shared.log("No JavaScript function extractDetails found",type: "Error")
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
||||
guard let extractEpisodesFunction = context.objectForKeyedSubscript("extractEpisodes") else {
|
||||
Logger.shared.log("No JavaScript function extractEpisodes found",type: "Error")
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
||||
var resultItems: [MediaItem] = []
|
||||
var episodeLinks: [EpisodeLink] = []
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
dispatchGroup.enter()
|
||||
let promiseValueDetails = extractDetailsFunction.call(withArguments: [url.absoluteString])
|
||||
guard let promiseDetails = promiseValueDetails else {
|
||||
Logger.shared.log("extractDetails did not return a Promise",type: "Error")
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
||||
let thenBlockDetails: @convention(block) (JSValue) -> Void = { result in
|
||||
Logger.shared.log(result.toString(),type: "Debug")
|
||||
if let jsonOfDetails = result.toString(),
|
||||
let dataDetails = jsonOfDetails.data(using: .utf8) {
|
||||
do {
|
||||
if let array = try JSONSerialization.jsonObject(with: dataDetails, options: []) as? [[String: Any]] {
|
||||
resultItems = array.map { item -> MediaItem in
|
||||
MediaItem(
|
||||
description: item["description"] as? String ?? "",
|
||||
aliases: item["aliases"] as? String ?? "",
|
||||
airdate: item["airdate"] as? String ?? ""
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse JSON of extractDetails",type: "Error")
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("JSON parsing error of extract details: \(error)",type: "Error")
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Result is not a string of extractDetails",type: "Error")
|
||||
}
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
let catchBlockDetails: @convention(block) (JSValue) -> Void = { error in
|
||||
Logger.shared.log("Promise rejected of extractDetails: \(String(describing: error.toString()))",type: "Error")
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
let thenFunctionDetails = JSValue(object: thenBlockDetails, in: context)
|
||||
let catchFunctionDetails = JSValue(object: catchBlockDetails, in: context)
|
||||
|
||||
promiseDetails.invokeMethod("then", withArguments: [thenFunctionDetails as Any])
|
||||
promiseDetails.invokeMethod("catch", withArguments: [catchFunctionDetails as Any])
|
||||
|
||||
dispatchGroup.enter()
|
||||
let promiseValueEpisodes = extractEpisodesFunction.call(withArguments: [url.absoluteString])
|
||||
guard let promiseEpisodes = promiseValueEpisodes else {
|
||||
Logger.shared.log("extractEpisodes did not return a Promise",type: "Error")
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
||||
let thenBlockEpisodes: @convention(block) (JSValue) -> Void = { result in
|
||||
Logger.shared.log(result.toString(),type: "Debug")
|
||||
if let jsonOfEpisodes = result.toString(),
|
||||
let dataEpisodes = jsonOfEpisodes.data(using: .utf8) {
|
||||
do {
|
||||
if let array = try JSONSerialization.jsonObject(with: dataEpisodes, options: []) as? [[String: Any]] {
|
||||
episodeLinks = array.map { item -> EpisodeLink in
|
||||
EpisodeLink(
|
||||
number: item["number"] as? Int ?? 0,
|
||||
href: item["href"] as? String ?? ""
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse JSON of extractEpisodes",type: "Error")
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("JSON parsing error of extractEpisodes: \(error)",type: "Error")
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Result is not a string of extractEpisodes",type: "Error")
|
||||
}
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
let catchBlockEpisodes: @convention(block) (JSValue) -> Void = { error in
|
||||
Logger.shared.log("Promise rejected of extractEpisodes: \(String(describing: error.toString()))",type: "Error")
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
let thenFunctionEpisodes = JSValue(object: thenBlockEpisodes, in: context)
|
||||
let catchFunctionEpisodes = JSValue(object: catchBlockEpisodes, in: context)
|
||||
|
||||
promiseEpisodes.invokeMethod("then", withArguments: [thenFunctionEpisodes as Any])
|
||||
promiseEpisodes.invokeMethod("catch", withArguments: [catchFunctionEpisodes as Any])
|
||||
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
completion(resultItems, episodeLinks)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchStreamUrlJS(episodeUrl: String, softsub: Bool = false, completion: @escaping ((stream: String?, subtitles: String?)) -> Void) {
|
||||
if let exception = context.exception {
|
||||
Logger.shared.log("JavaScript exception: \(exception)", type: "Error")
|
||||
completion((nil, nil))
|
||||
return
|
||||
}
|
||||
|
||||
guard let extractStreamUrlFunction = context.objectForKeyedSubscript("extractStreamUrl") else {
|
||||
Logger.shared.log("No JavaScript function extractStreamUrl found", type: "Error")
|
||||
completion((nil, nil))
|
||||
return
|
||||
}
|
||||
|
||||
let promiseValue = extractStreamUrlFunction.call(withArguments: [episodeUrl])
|
||||
guard let promise = promiseValue else {
|
||||
Logger.shared.log("extractStreamUrl did not return a Promise", type: "Error")
|
||||
completion((nil, nil))
|
||||
return
|
||||
}
|
||||
|
||||
let thenBlock: @convention(block) (JSValue) -> Void = { result in
|
||||
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 streamUrl = json["stream"] as? String
|
||||
let subtitlesUrl = json["subtitles"] as? String
|
||||
Logger.shared.log("Starting stream from: \(streamUrl ?? "nil") with subtitles: \(subtitlesUrl ?? "nil")", type: "Stream")
|
||||
DispatchQueue.main.async {
|
||||
completion((streamUrl, subtitlesUrl))
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse softsub JSON in JS", 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let catchBlock: @convention(block) (JSValue) -> Void = { error in
|
||||
Logger.shared.log("Promise rejected: \(String(describing: error.toString()))", type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion((nil, nil))
|
||||
}
|
||||
}
|
||||
|
||||
let thenFunction = JSValue(object: thenBlock, in: context)
|
||||
let catchFunction = JSValue(object: catchBlock, in: context)
|
||||
|
||||
promise.invokeMethod("then", withArguments: [thenFunction as Any])
|
||||
promise.invokeMethod("catch", withArguments: [catchFunction as Any])
|
||||
}
|
||||
|
||||
func fetchStreamUrlJSSecond(episodeUrl: String, softsub: Bool = false, completion: @escaping ((stream: String?, subtitles: String?)) -> Void) {
|
||||
let url = URL(string: episodeUrl)!
|
||||
let task = URLSession.custom.dataTask(with: url) { data, response, error in
|
||||
if let error = error {
|
||||
Logger.shared.log("URLSession error: \(error.localizedDescription)", type: "Error")
|
||||
DispatchQueue.main.async { completion((nil, nil)) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let htmlString = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to fetch HTML data", type: "Error")
|
||||
DispatchQueue.main.async { completion((nil, nil)) }
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if let exception = self.context.exception {
|
||||
Logger.shared.log("JavaScript exception: \(exception)", type: "Error")
|
||||
completion((nil, nil))
|
||||
return
|
||||
}
|
||||
|
||||
guard let extractStreamUrlFunction = self.context.objectForKeyedSubscript("extractStreamUrl") else {
|
||||
Logger.shared.log("No JavaScript function extractStreamUrl found", type: "Error")
|
||||
completion((nil, nil))
|
||||
return
|
||||
}
|
||||
|
||||
let promiseValue = extractStreamUrlFunction.call(withArguments: [htmlString])
|
||||
guard let promise = promiseValue else {
|
||||
Logger.shared.log("extractStreamUrl did not return a Promise", type: "Error")
|
||||
completion((nil, nil))
|
||||
return
|
||||
}
|
||||
|
||||
let thenBlock: @convention(block) (JSValue) -> Void = { result in
|
||||
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 streamUrl = json["stream"] as? String
|
||||
let subtitlesUrl = json["subtitles"] as? String
|
||||
Logger.shared.log("Starting stream from: \(streamUrl ?? "nil") with subtitles: \(subtitlesUrl ?? "nil")", type: "Stream")
|
||||
DispatchQueue.main.async {
|
||||
completion((streamUrl, subtitlesUrl))
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse softsub JSON in JSSecond", 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let catchBlock: @convention(block) (JSValue) -> Void = { error in
|
||||
Logger.shared.log("Promise rejected: \(String(describing: error.toString()))", type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion((nil, nil))
|
||||
}
|
||||
}
|
||||
|
||||
let thenFunction = JSValue(object: thenBlock, in: self.context)
|
||||
let catchFunction = JSValue(object: catchBlock, in: self.context)
|
||||
|
||||
promise.invokeMethod("then", withArguments: [thenFunction as Any])
|
||||
promise.invokeMethod("catch", withArguments: [catchFunction as Any])
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ struct ModuleMetadata: Codable, Hashable {
|
|||
let asyncJS: Bool?
|
||||
let streamAsyncJS: Bool?
|
||||
let softsub: Bool?
|
||||
let multiStream: Bool?
|
||||
let multiSubs: Bool?
|
||||
|
||||
struct Author: Codable, Hashable {
|
||||
let name: String
|
||||
|
|
|
|||
|
|
@ -500,8 +500,9 @@ struct MediaInfoView: View {
|
|||
if module.metadata.softsub == true {
|
||||
if module.metadata.asyncJS == true {
|
||||
jsController.fetchStreamUrlJS(episodeUrl: href, softsub: true) { result in
|
||||
if let streamUrl = result.stream {
|
||||
self.playStream(url: streamUrl, fullURL: href, subtitles: result.subtitles)
|
||||
// Use first stream from array
|
||||
if let streams = result.streams, !streams.isEmpty {
|
||||
self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first)
|
||||
} else {
|
||||
self.handleStreamFailure(error: nil)
|
||||
}
|
||||
|
|
@ -511,8 +512,8 @@ struct MediaInfoView: View {
|
|||
}
|
||||
} else if module.metadata.streamAsyncJS == true {
|
||||
jsController.fetchStreamUrlJSSecond(episodeUrl: href, softsub: true) { result in
|
||||
if let streamUrl = result.stream {
|
||||
self.playStream(url: streamUrl, fullURL: href, subtitles: result.subtitles)
|
||||
if let streams = result.streams, !streams.isEmpty {
|
||||
self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first)
|
||||
} else {
|
||||
self.handleStreamFailure(error: nil)
|
||||
}
|
||||
|
|
@ -522,8 +523,8 @@ struct MediaInfoView: View {
|
|||
}
|
||||
} else {
|
||||
jsController.fetchStreamUrl(episodeUrl: href, softsub: true) { result in
|
||||
if let streamUrl = result.stream {
|
||||
self.playStream(url: streamUrl, fullURL: href, subtitles: result.subtitles)
|
||||
if let streams = result.streams, !streams.isEmpty {
|
||||
self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first)
|
||||
} else {
|
||||
self.handleStreamFailure(error: nil)
|
||||
}
|
||||
|
|
@ -535,8 +536,8 @@ struct MediaInfoView: View {
|
|||
} else {
|
||||
if module.metadata.asyncJS == true {
|
||||
jsController.fetchStreamUrlJS(episodeUrl: href) { result in
|
||||
if let streamUrl = result.stream {
|
||||
self.playStream(url: streamUrl, fullURL: href)
|
||||
if let streams = result.streams, !streams.isEmpty {
|
||||
self.playStream(url: streams[0], fullURL: href)
|
||||
} else {
|
||||
self.handleStreamFailure(error: nil)
|
||||
}
|
||||
|
|
@ -546,8 +547,8 @@ struct MediaInfoView: View {
|
|||
}
|
||||
} else if module.metadata.streamAsyncJS == true {
|
||||
jsController.fetchStreamUrlJSSecond(episodeUrl: href) { result in
|
||||
if let streamUrl = result.stream {
|
||||
self.playStream(url: streamUrl, fullURL: href)
|
||||
if let streams = result.streams, !streams.isEmpty {
|
||||
self.playStream(url: streams[0], fullURL: href)
|
||||
} else {
|
||||
self.handleStreamFailure(error: nil)
|
||||
}
|
||||
|
|
@ -557,8 +558,8 @@ struct MediaInfoView: View {
|
|||
}
|
||||
} else {
|
||||
jsController.fetchStreamUrl(episodeUrl: href) { result in
|
||||
if let streamUrl = result.stream {
|
||||
self.playStream(url: streamUrl, fullURL: href)
|
||||
if let streams = result.streams, !streams.isEmpty {
|
||||
self.playStream(url: streams[0], fullURL: href)
|
||||
} else {
|
||||
self.handleStreamFailure(error: nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@
|
|||
131845F92D47C62D00CA7A54 /* SettingsViewGeneral.swift in Sources */ = {isa = PBXBuildFile; fileRef = 131845F82D47C62D00CA7A54 /* SettingsViewGeneral.swift */; };
|
||||
1327FBA72D758CEA00FC6689 /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1327FBA62D758CEA00FC6689 /* Analytics.swift */; };
|
||||
1327FBA92D758DEA00FC6689 /* UIDevice+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1327FBA82D758DEA00FC6689 /* UIDevice+Model.swift */; };
|
||||
132AF1212D99951700A0140B /* JSController-Streams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1202D99951700A0140B /* JSController-Streams.swift */; };
|
||||
132AF1232D9995C300A0140B /* JSController-Details.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1222D9995C300A0140B /* JSController-Details.swift */; };
|
||||
132AF1252D9995F900A0140B /* JSController-Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1242D9995F900A0140B /* JSController-Search.swift */; };
|
||||
132E351D2D959DDB0007800E /* Drops in Frameworks */ = {isa = PBXBuildFile; productRef = 132E351C2D959DDB0007800E /* Drops */; };
|
||||
132E35202D959E1D0007800E /* FFmpeg-iOS-Lame in Frameworks */ = {isa = PBXBuildFile; productRef = 132E351F2D959E1D0007800E /* FFmpeg-iOS-Lame */; };
|
||||
132E35232D959E410007800E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 132E35222D959E410007800E /* Kingfisher */; };
|
||||
|
|
@ -74,6 +77,9 @@
|
|||
131845F82D47C62D00CA7A54 /* SettingsViewGeneral.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewGeneral.swift; sourceTree = "<group>"; };
|
||||
1327FBA62D758CEA00FC6689 /* Analytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Analytics.swift; sourceTree = "<group>"; };
|
||||
1327FBA82D758DEA00FC6689 /* UIDevice+Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+Model.swift"; sourceTree = "<group>"; };
|
||||
132AF1202D99951700A0140B /* JSController-Streams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Streams.swift"; sourceTree = "<group>"; };
|
||||
132AF1222D9995C300A0140B /* JSController-Details.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Details.swift"; sourceTree = "<group>"; };
|
||||
132AF1242D9995F900A0140B /* JSController-Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Search.swift"; sourceTree = "<group>"; };
|
||||
1334FF4C2D786C93007E289F /* TMDB-Seasonal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TMDB-Seasonal.swift"; sourceTree = "<group>"; };
|
||||
1334FF4E2D786C9E007E289F /* TMDB-Trending.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TMDB-Trending.swift"; sourceTree = "<group>"; };
|
||||
1334FF512D7871B7007E289F /* TMDBItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TMDBItem.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -326,6 +332,9 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
133D7C8B2D2BE2640075467E /* JSController.swift */,
|
||||
132AF1202D99951700A0140B /* JSController-Streams.swift */,
|
||||
132AF1222D9995C300A0140B /* JSController-Details.swift */,
|
||||
132AF1242D9995F900A0140B /* JSController-Search.swift */,
|
||||
);
|
||||
path = JSLoader;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -524,6 +533,7 @@
|
|||
13B7F4C12D58FFDD0045714A /* Shimmer.swift in Sources */,
|
||||
139935662D468C450065CEFF /* ModuleManager.swift in Sources */,
|
||||
133D7C902D2BE2640075467E /* SettingsView.swift in Sources */,
|
||||
132AF1252D9995F900A0140B /* JSController-Search.swift in Sources */,
|
||||
1334FF4F2D786C9E007E289F /* TMDB-Trending.swift in Sources */,
|
||||
13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */,
|
||||
13DB46902D900A38008CBC03 /* URL.swift in Sources */,
|
||||
|
|
@ -542,6 +552,7 @@
|
|||
13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */,
|
||||
133D7C8F2D2BE2640075467E /* MediaInfoView.swift in Sources */,
|
||||
13103E892D58A39A000F0673 /* AniListItem.swift in Sources */,
|
||||
132AF1212D99951700A0140B /* JSController-Streams.swift in Sources */,
|
||||
131845F92D47C62D00CA7A54 /* SettingsViewGeneral.swift in Sources */,
|
||||
13103E8E2D58E04A000F0673 /* SkeletonCell.swift in Sources */,
|
||||
13D842552D45267500EBBFA6 /* DropManager.swift in Sources */,
|
||||
|
|
@ -562,6 +573,7 @@
|
|||
1334FF4D2D786C93007E289F /* TMDB-Seasonal.swift in Sources */,
|
||||
13DB468D2D90093A008CBC03 /* Anilist-Login.swift in Sources */,
|
||||
73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */,
|
||||
132AF1232D9995C300A0140B /* JSController-Details.swift in Sources */,
|
||||
13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */,
|
||||
1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */,
|
||||
13DB46922D900BCE008CBC03 /* SettingsViewTrackers.swift in Sources */,
|
||||
|
|
|
|||
Loading…
Reference in a new issue