diff --git a/Sora/Utlis & Misc/JSLoader/JSController-Details.swift b/Sora/Utlis & Misc/JSLoader/JSController-Details.swift index 9c79f0f..ea2b8ae 100644 --- a/Sora/Utlis & Misc/JSLoader/JSController-Details.swift +++ b/Sora/Utlis & Misc/JSLoader/JSController-Details.swift @@ -9,7 +9,6 @@ import Foundation import JavaScriptCore extension JSController { - func fetchDetails(url: String, completion: @escaping ([MediaItem], [EpisodeLink]) -> Void) { guard let url = URL(string: url) else { completion([], []) @@ -94,41 +93,64 @@ extension JSController { let dispatchGroup = DispatchGroup() dispatchGroup.enter() + var hasLeftDetailsGroup = false + let detailsGroupQueue = DispatchQueue(label: "details.group") + let promiseValueDetails = extractDetailsFunction.call(withArguments: [url.absoluteString]) guard let promiseDetails = promiseValueDetails else { Logger.shared.log("extractDetails did not return a Promise", type: "Error") - dispatchGroup.leave() + detailsGroupQueue.sync { + guard !hasLeftDetailsGroup else { return } + hasLeftDetailsGroup = true + dispatchGroup.leave() + } completion([], []) return } let thenBlockDetails: @convention(block) (JSValue) -> Void = { result in - 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") + detailsGroupQueue.sync { + guard !hasLeftDetailsGroup else { + Logger.shared.log("extractDetails: thenBlock called but group already left", type: "Debug") + return } - } else { - Logger.shared.log("Result is not a string of extractDetails", type: "Error") + hasLeftDetailsGroup = true + + 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() } - 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() + detailsGroupQueue.sync { + guard !hasLeftDetailsGroup else { + Logger.shared.log("extractDetails: catchBlock called but group already left", type: "Debug") + return + } + hasLeftDetailsGroup = true + + Logger.shared.log("Promise rejected of extractDetails: \(String(describing: error.toString()))", type: "Error") + dispatchGroup.leave() + } } let thenFunctionDetails = JSValue(object: thenBlockDetails, in: context) @@ -140,50 +162,80 @@ extension JSController { dispatchGroup.enter() let promiseValueEpisodes = extractEpisodesFunction.call(withArguments: [url.absoluteString]) + var hasLeftEpisodesGroup = false + let episodesGroupQueue = DispatchQueue(label: "episodes.group") + let timeoutWorkItem = DispatchWorkItem { Logger.shared.log("Timeout for extractEpisodes", type: "Warning") - dispatchGroup.leave() + episodesGroupQueue.sync { + guard !hasLeftEpisodesGroup else { + Logger.shared.log("extractEpisodes: timeout called but group already left", type: "Debug") + return + } + hasLeftEpisodesGroup = true + dispatchGroup.leave() + } } DispatchQueue.main.asyncAfter(deadline: .now() + 5.0, execute: timeoutWorkItem) guard let promiseEpisodes = promiseValueEpisodes else { Logger.shared.log("extractEpisodes did not return a Promise", type: "Error") timeoutWorkItem.cancel() - dispatchGroup.leave() + episodesGroupQueue.sync { + guard !hasLeftEpisodesGroup else { return } + hasLeftEpisodesGroup = true + dispatchGroup.leave() + } completion([], []) return } let thenBlockEpisodes: @convention(block) (JSValue) -> Void = { result in timeoutWorkItem.cancel() - 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, - title: "", - href: item["href"] as? String ?? "", - duration: nil - ) - } - } else { - Logger.shared.log("Failed to parse JSON of extractEpisodes", type: "Error") - } - } catch { - Logger.shared.log("JSON parsing error of extractEpisodes: \(error)", type: "Error") + episodesGroupQueue.sync { + guard !hasLeftEpisodesGroup else { + Logger.shared.log("extractEpisodes: thenBlock called but group already left", type: "Debug") + return } - } else { - Logger.shared.log("Result is not a string of extractEpisodes", type: "Error") + hasLeftEpisodesGroup = true + + 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, + title: "", + href: item["href"] as? String ?? "", + duration: nil + ) + } + } 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() } - dispatchGroup.leave() } let catchBlockEpisodes: @convention(block) (JSValue) -> Void = { error in timeoutWorkItem.cancel() - Logger.shared.log("Promise rejected of extractEpisodes: \(String(describing: error.toString()))", type: "Error") - dispatchGroup.leave() + episodesGroupQueue.sync { + guard !hasLeftEpisodesGroup else { + Logger.shared.log("extractEpisodes: catchBlock called but group already left", type: "Debug") + return + } + hasLeftEpisodesGroup = true + + Logger.shared.log("Promise rejected of extractEpisodes: \(String(describing: error.toString()))", type: "Error") + dispatchGroup.leave() + } } let thenFunctionEpisodes = JSValue(object: thenBlockEpisodes, in: context) diff --git a/Sora/Utlis & Misc/JSLoader/JSController-Novel.swift b/Sora/Utlis & Misc/JSLoader/JSController-Novel.swift index ab446fc..12d04c4 100644 --- a/Sora/Utlis & Misc/JSLoader/JSController-Novel.swift +++ b/Sora/Utlis & Misc/JSLoader/JSController-Novel.swift @@ -59,30 +59,48 @@ extension JSController { let group = DispatchGroup() group.enter() var chaptersArr: [[String: Any]] = [] + var hasLeftGroup = false + let groupQueue = DispatchQueue(label: "extractChapters.group") + let thenBlock: @convention(block) (JSValue) -> Void = { jsValue in Logger.shared.log("extractChapters thenBlock: \(jsValue)", type: "Debug") - if let arr = jsValue.toArray() as? [[String: Any]] { - Logger.shared.log("extractChapters: parsed as array, count = \(arr.count)", type: "Debug") - chaptersArr = arr - } else if let jsonString = jsValue.toString(), let data = jsonString.data(using: .utf8) { - do { - if let arr = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] { - Logger.shared.log("extractChapters: parsed as JSON string, count = \(arr.count)", type: "Debug") - chaptersArr = arr - } else { - Logger.shared.log("extractChapters: JSON string did not parse to array", type: "Error") - } - } catch { - Logger.shared.log("JSON parsing error of extractChapters: \(error)", type: "Error") + groupQueue.sync { + guard !hasLeftGroup else { + Logger.shared.log("extractChapters: thenBlock called but group already left", type: "Debug") + return } - } else { - Logger.shared.log("extractChapters: could not parse result", type: "Error") + hasLeftGroup = true + + if let arr = jsValue.toArray() as? [[String: Any]] { + Logger.shared.log("extractChapters: parsed as array, count = \(arr.count)", type: "Debug") + chaptersArr = arr + } else if let jsonString = jsValue.toString(), let data = jsonString.data(using: .utf8) { + do { + if let arr = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] { + Logger.shared.log("extractChapters: parsed as JSON string, count = \(arr.count)", type: "Debug") + chaptersArr = arr + } else { + Logger.shared.log("extractChapters: JSON string did not parse to array", type: "Error") + } + } catch { + Logger.shared.log("JSON parsing error of extractChapters: \(error)", type: "Error") + } + } else { + Logger.shared.log("extractChapters: could not parse result", type: "Error") + } + group.leave() } - group.leave() } let catchBlock: @convention(block) (JSValue) -> Void = { jsValue in Logger.shared.log("extractChapters catchBlock: \(jsValue)", type: "Error") - group.leave() + groupQueue.sync { + guard !hasLeftGroup else { + Logger.shared.log("extractChapters: catchBlock called but group already left", type: "Debug") + return + } + hasLeftGroup = true + group.leave() + } } result.invokeMethod("then", withArguments: [thenBlock]) result.invokeMethod("catch", withArguments: [catchBlock]) @@ -182,24 +200,42 @@ extension JSController { group.enter() var extractedText = "" var extractError: Error? = nil + var hasLeftGroup = false + let groupQueue = DispatchQueue(label: "extractText.group") let thenBlock: @convention(block) (JSValue) -> Void = { jsValue in Logger.shared.log("extractText thenBlock: received value", type: "Debug") - if let text = jsValue.toString(), !text.isEmpty { - Logger.shared.log("extractText: successfully extracted text", type: "Debug") - extractedText = text - } else { - extractError = JSError.emptyContent + groupQueue.sync { + guard !hasLeftGroup else { + Logger.shared.log("extractText: thenBlock called but group already left", type: "Debug") + return + } + hasLeftGroup = true + + if let text = jsValue.toString(), !text.isEmpty { + Logger.shared.log("extractText: successfully extracted text", type: "Debug") + extractedText = text + } else { + extractError = JSError.emptyContent + } + group.leave() } - group.leave() } let catchBlock: @convention(block) (JSValue) -> Void = { jsValue in Logger.shared.log("extractText catchBlock: \(jsValue)", type: "Error") - if extractedText.isEmpty { - extractError = JSError.jsException(jsValue.toString() ?? "Unknown error") + groupQueue.sync { + guard !hasLeftGroup else { + Logger.shared.log("extractText: catchBlock called but group already left", type: "Debug") + return + } + hasLeftGroup = true + + if extractedText.isEmpty { + extractError = JSError.jsException(jsValue.toString() ?? "Unknown error") + } + group.leave() } - group.leave() } result.invokeMethod("then", withArguments: [thenBlock]) @@ -277,8 +313,8 @@ extension JSController { let (data, response) = try await URLSession.shared.data(for: request) - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { + guard let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode) else { Logger.shared.log("Direct fetch failed with status code: \((response as? HTTPURLResponse)?.statusCode ?? -1)", type: "Error") throw JSError.invalidResponse } @@ -317,4 +353,4 @@ extension JSController { Logger.shared.log("Direct fetch successful, content length: \(content.count)", type: "Debug") return content } -} +} diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index d487e4d..ee3085f 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -341,6 +341,8 @@ struct SettingsViewGeneral: View { .font(.caption) .foregroundStyle(.gray) .frame(maxWidth: .infinity, alignment: .center) + .padding(.top, -6) + .padding(.bottom, 8) } .environment(\.editMode, .constant(.active)) } diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index eb72bea..c9bf7ef 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -634,8 +634,8 @@ isa = PBXGroup; children = ( 134A387B2DE4B5B90041B687 /* Downloads */, - 04536F702E04BA3B00A11248 /* JSController-Novel.swift */, 133D7C8B2D2BE2640075467E /* JSController.swift */, + 04536F702E04BA3B00A11248 /* JSController-Novel.swift */, 132AF1202D99951700A0140B /* JSController-Streams.swift */, 132AF1222D9995C300A0140B /* JSController-Details.swift */, 132AF1242D9995F900A0140B /* JSController-Search.swift */,