diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 0318843..bdd09ff 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -440,7 +440,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Author:" + "value" : "Author: %@" } }, "en" : { diff --git a/Sora/Utils/JSLoader/JSController-Explore.swift b/Sora/Utils/JSLoader/JSController-Explore.swift index cf712e1..b46b08d 100644 --- a/Sora/Utils/JSLoader/JSController-Explore.swift +++ b/Sora/Utils/JSLoader/JSController-Explore.swift @@ -10,34 +10,32 @@ import JavaScriptCore // TODO: implement and test extension JSController { func fetchExploreResults(module: ScrapingModule, completion: @escaping ([ExploreItem]) -> Void) { - completion([]) - /*let searchUrl = module.metadata.searchBaseUrl.replacingOccurrences(of: "%s", with: keyword.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "") - - guard let url = URL(string: searchUrl) else { + guard let exploreUrl = module.metadata.exploreBaseUrl, + let url = URL(string: exploreUrl) else { completion([]) return } - + URLSession.custom.dataTask(with: url) { [weak self] data, _, error in - guard let self = self else { return } - + guard let self else { return } + if let error { - Logger.shared.log("Network error: \(error)",type: "Error") + Logger.shared.log("Network error: \(error)", type: .error) DispatchQueue.main.async { completion([]) } return } - + guard let data, let html = String(data: data, encoding: .utf8) else { - Logger.shared.log("Failed to decode HTML",type: "Error") + 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"), + + Logger.shared.log(html, type: .html) + if let parseFunction = self.context.objectForKeyedSubscript("exploreResults"), let results = parseFunction.call(withArguments: [html]).toArray() as? [[String: String]] { let resultItems = results.map { item in - SearchItem( + ExploreItem( title: item["title"] ?? "", imageUrl: item["image"] ?? "", href: item["href"] ?? "" @@ -47,88 +45,82 @@ extension JSController { completion(resultItems) } } else { - Logger.shared.log("Failed to parse results",type: "Error") + Logger.shared.log("Failed to parse results", type: .error) DispatchQueue.main.async { completion([]) } } }.resume() - */ } func fetchJsExploreResults(module: ScrapingModule, completion: @escaping ([ExploreItem]) -> Void) { - completion([]) - /* if let exception = context.exception { - Logger.shared.log("JavaScript exception: \(exception)",type: "Error") + 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") + + guard let exploreResultsFunction = context.objectForKeyedSubscript("exploreResults") else { + Logger.shared.log("No JavaScript function exploreResults found", type: .error) completion([]) return } - - let promiseValue = searchResultsFunction.call(withArguments: [keyword]) + + let promiseValue = exploreResultsFunction.call(withArguments: []) guard let promise = promiseValue else { - Logger.shared.log("searchResults did not return a Promise",type: "Error") + Logger.shared.log("exploreResults did not return a Promise", type: .error) completion([]) return } - + let thenBlock: @convention(block) (JSValue) -> Void = { result in - - Logger.shared.log(result.toString(),type: "HTMLStrings") + Logger.shared.log(result.toString(), type: .html) 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 + let resultItems = array.compactMap { item -> ExploreItem? 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) + return ExploreItem(title: title, imageUrl: imageUrl, href: href) } - + DispatchQueue.main.async { completion(resultItems) } - } else { - Logger.shared.log("Failed to parse JSON",type: "Error") + Logger.shared.log("Failed to parse JSON", type: .error) DispatchQueue.main.async { completion([]) } } } catch { - Logger.shared.log("JSON parsing error: \(error)",type: "Error") + Logger.shared.log("JSON parsing error: \(error)", type: .error) DispatchQueue.main.async { completion([]) } } } else { - Logger.shared.log("Result is not a string",type: "Error") + 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") + 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]) - */ } } diff --git a/Sora/Utils/Modules/ModuleManager.swift b/Sora/Utils/Modules/ModuleManager.swift index e00dd45..02c7ccb 100644 --- a/Sora/Utils/Modules/ModuleManager.swift +++ b/Sora/Utils/Modules/ModuleManager.swift @@ -172,9 +172,26 @@ class ModuleManager: ObservableObject { let localUrl = getDocumentsDirectory().appendingPathComponent(fileName) try jsContent.write(to: localUrl, atomically: true, encoding: .utf8) + var exploreFileName: String? + if let exploreScriptValue = metadata.exploreScriptUrl { + guard let exploreScriptUrl = URL(string: exploreScriptValue) else { + throw NSError(domain: "Invalid script URL", code: -1) + } + + let (exploreScriptData, _) = try await URLSession.custom.data(from: exploreScriptUrl) + guard let exploreJsContent = String(data: exploreScriptData, encoding: .utf8) else { + throw NSError(domain: "Invalid script encoding", code: -1) + } + + exploreFileName = "\(UUID().uuidString).js" + let exploreLocalUrl = getDocumentsDirectory().appendingPathComponent(exploreFileName!) + try exploreJsContent.write(to: exploreLocalUrl, atomically: true, encoding: .utf8) + } + let module = ScrapingModule( metadata: metadata, localPath: fileName, + exploreLocalPath: exploreFileName, metadataUrl: metadataUrl ) @@ -201,6 +218,15 @@ class ModuleManager: ObservableObject { return try String(contentsOf: localUrl, encoding: .utf8) } + func getModuleExploreContent(_ module: ScrapingModule) throws -> String? { + if let exploreLocalPath = module.exploreLocalPath { + let exploreLocalUrl = getDocumentsDirectory().appendingPathComponent(exploreLocalPath) + return try String(contentsOf: exploreLocalUrl, encoding: .utf8) + } else { + return nil + } + } + func refreshModules() async { for (index, module) in modules.enumerated() { do { @@ -220,10 +246,27 @@ class ModuleManager: ObservableObject { let localUrl = getDocumentsDirectory().appendingPathComponent(module.localPath) try jsContent.write(to: localUrl, atomically: true, encoding: .utf8) + var exploreFileName: String? + if let exploreScriptValue = newMetadata.exploreScriptUrl { + guard let exploreScriptUrl = URL(string: exploreScriptValue) else { + throw NSError(domain: "Invalid script URL", code: -1) + } + + let (exploreScriptData, _) = try await URLSession.custom.data(from: exploreScriptUrl) + guard let exploreJsContent = String(data: exploreScriptData, encoding: .utf8) else { + throw NSError(domain: "Invalid script encoding", code: -1) + } + + exploreFileName = module.exploreLocalPath ?? "\(UUID().uuidString).js" + let exploreLocalUrl = getDocumentsDirectory().appendingPathComponent(exploreFileName!) + try exploreJsContent.write(to: exploreLocalUrl, atomically: true, encoding: .utf8) + } + let updatedModule = ScrapingModule( id: module.id, metadata: newMetadata, localPath: module.localPath, + exploreLocalPath: exploreFileName, metadataUrl: module.metadataUrl, isActive: module.isActive ) diff --git a/Sora/Utils/Modules/Modules.swift b/Sora/Utils/Modules/Modules.swift index 28fe10a..b62dc54 100644 --- a/Sora/Utils/Modules/Modules.swift +++ b/Sora/Utils/Modules/Modules.swift @@ -18,6 +18,8 @@ struct ModuleMetadata: Codable, Hashable { let quality: String let searchBaseUrl: String let scriptUrl: String + let exploreBaseUrl: String? + let exploreScriptUrl: String? let asyncJS: Bool? let streamAsyncJS: Bool? let softsub: Bool? @@ -35,13 +37,22 @@ struct ScrapingModule: Codable, Identifiable, Hashable { let id: UUID let metadata: ModuleMetadata let localPath: String + let exploreLocalPath: String? let metadataUrl: String var isActive: Bool - init(id: UUID = UUID(), metadata: ModuleMetadata, localPath: String, metadataUrl: String, isActive: Bool = false) { + init( + id: UUID = UUID(), + metadata: ModuleMetadata, + localPath: String, + exploreLocalPath: String?, + metadataUrl: String, + isActive: Bool = false + ) { self.id = id self.metadata = metadata self.localPath = localPath + self.exploreLocalPath = exploreLocalPath self.metadataUrl = metadataUrl self.isActive = isActive } diff --git a/Sora/Views/ExploreView/ExploreView.swift b/Sora/Views/ExploreView/ExploreView.swift index a849b05..adfe4d5 100644 --- a/Sora/Views/ExploreView/ExploreView.swift +++ b/Sora/Views/ExploreView/ExploreView.swift @@ -284,7 +284,12 @@ struct ExploreView: View { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { Task { do { - let jsContent = try moduleManager.getModuleContent(module) + guard let jsContent = try moduleManager.getModuleExploreContent(module) else { + isLoading = false + hasNoResults = true + return + } + jsController.loadScript(jsContent) if module.metadata.asyncJS == true { jsController.fetchJsExploreResults(module: module) { items in