more linting ( 0 errors and warnings ), logger types, cleanup

This commit is contained in:
Dominic Drees 2025-05-02 01:43:08 +02:00
parent ee11dc81c1
commit 63724c6ef0
36 changed files with 801 additions and 454 deletions

View file

@ -64,12 +64,15 @@ disabled_rules:
- discouraged_none_name
- attributes
- prefer_key_path
- block_based_kvo
- for_where
# should be fixed sometimes:
- implicitly_unwrapped_optional
- cyclomatic_complexity
- unused_parameter
- fatal_error
- force_cast
- object_literal
large_tuple: 3
number_separator:

View file

@ -100,7 +100,20 @@
}
},
"+ Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Plus Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Plus Icon"
}
}
}
},
"+10s" : {
"localizations" : {
@ -199,7 +212,20 @@
}
},
"Alternative App Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Alternatives App Icon Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Alternative App Icon"
}
}
}
},
"AniList" : {
"localizations" : {
@ -218,7 +244,20 @@
}
},
"AniList Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "AniList Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "AniList Icon"
}
}
}
},
"AniList.co" : {
"localizations" : {
@ -444,6 +483,22 @@
}
}
},
"Bookmark Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Lesezeichen Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bookmark Icon"
}
}
}
},
"Bookmark items for an easier access later." : {
"localizations" : {
"de" : {
@ -492,6 +547,22 @@
}
}
},
"Calendar Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kalendar Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Calendar Icon"
}
}
}
},
"Cancel" : {
"localizations" : {
"de" : {
@ -525,7 +596,20 @@
}
},
"Checkmark Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ausgewählt Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Checkmark Icon"
}
}
}
},
"Clear Cache" : {
"localizations" : {
@ -544,7 +628,20 @@
}
},
"Clear Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Löschen Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Clear Icon"
}
}
}
},
"Clear Logs" : {
"localizations" : {
@ -595,7 +692,20 @@
}
},
"Configure Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Konfigurieren Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Configure Icon"
}
}
}
},
"Confirm Erase App Data" : {
"localizations" : {
@ -934,7 +1044,20 @@
}
},
"Episode Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Episoden Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Episode Icon"
}
}
}
},
"Episodes" : {
"localizations" : {
@ -1017,7 +1140,20 @@
}
},
"Error Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Fehler Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Error Icon"
}
}
}
},
"Error: %@" : {
"localizations" : {
@ -1053,7 +1189,20 @@
}
},
"Expand Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Aufklappen Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Expand Icon"
}
}
}
},
"Explore" : {
"localizations" : {
@ -1088,7 +1237,20 @@
}
},
"External App Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Externe App Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "External App Icon"
}
}
}
},
"Failed to parse response" : {
"localizations" : {
@ -1695,10 +1857,36 @@
}
},
"Magazine Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Buch Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Magazine Icon"
}
}
}
},
"Magnifying Glass Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Suchen Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Magnifying Glass Icon"
}
}
}
},
"Main" : {
"localizations" : {
@ -1845,7 +2033,20 @@
}
},
"More Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Mehr Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "More Icon"
}
}
}
},
"Name" : {
"localizations" : {
@ -1867,7 +2068,7 @@
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "needs_review",
"state" : "translated",
"value" : "Neuer Nutzer"
}
},
@ -2120,7 +2321,20 @@
}
},
"Play Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Abspielen Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Play Icon"
}
}
}
},
"Play Offline Content" : {
"localizations" : {
@ -2219,7 +2433,20 @@
}
},
"Questionmark Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Fragezeichen Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Questionmark Icon"
}
}
}
},
"Recently watched content will appear here." : {
"localizations" : {
@ -2397,6 +2624,22 @@
}
}
},
"Right Arrow Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pfeil nach Rechts Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Right Arrow Icon"
}
}
}
},
"Running Sora %@\nby cranci1" : {
"localizations" : {
"de" : {
@ -2430,7 +2673,20 @@
}
},
"Safari Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Safari Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Safari Icon"
}
}
}
},
"Scan to Visit" : {
"localizations" : {
@ -2465,7 +2721,20 @@
}
},
"Search Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Suchen Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Search Icon"
}
}
}
},
"Search View" : {
"localizations" : {
@ -2708,7 +2977,20 @@
}
},
"Speaker Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Lautsprecher Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Speaker Icon"
}
}
}
},
"Speed Settings" : {
"localizations" : {
@ -2727,7 +3009,20 @@
}
},
"Star Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Stern Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Star Icon"
}
}
}
},
"Starting authentication..." : {
"localizations" : {
@ -2906,7 +3201,20 @@
}
},
"Trakt Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Trakt Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Trakt Icon"
}
}
}
},
"Trakt.tv" : {
"localizations" : {
@ -3004,6 +3312,22 @@
}
}
},
"Warning Icon" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Warnung Icon"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Warning Icon"
}
}
}
},
"You are not logged in" : {
"localizations" : {
"de" : {

View file

@ -20,7 +20,7 @@ struct SoraApp: App {
if isAuthenticated {
Logger.shared.log("Trakt authentication is valid")
} else {
Logger.shared.log("Trakt authentication required", type: "Error")
Logger.shared.log("Trakt authentication required", type: .error)
}
}
}
@ -112,7 +112,7 @@ struct SoraApp: App {
} else {
Logger.shared.log(
"Failed to present module addition view: No window scene found",
type: "Error"
type: .error
)
}
@ -134,7 +134,7 @@ struct SoraApp: App {
if success {
Logger.shared.log("AniList token exchange successful")
} else {
Logger.shared.log("AniList token exchange failed", type: "Error")
Logger.shared.log("AniList token exchange failed", type: .error)
}
}
@ -143,12 +143,12 @@ struct SoraApp: App {
if success {
Logger.shared.log("Trakt token exchange successful")
} else {
Logger.shared.log("Trakt token exchange failed", type: "Error")
Logger.shared.log("Trakt token exchange failed", type: .error)
}
}
default:
Logger.shared.log("Unknown authentication service", type: "Error")
Logger.shared.log("Unknown authentication service", type: .error)
}
}
}

View file

@ -22,13 +22,13 @@ class AniListLogin {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:]) { success in
if success {
Logger.shared.log("Safari opened successfully", type: "Debug")
Logger.shared.log("Safari opened successfully", type: .debug)
} else {
Logger.shared.log("Failed to open Safari", type: "Error")
Logger.shared.log("Failed to open Safari", type: .error)
}
}
} else {
Logger.shared.log("Cannot open URL", type: "Error")
Logger.shared.log("Cannot open URL", type: .error)
}
}
}

View file

@ -45,7 +45,7 @@ class AniListToken {
Logger.shared.log("Exchanging authorization code for access token...")
guard let url = URL(string: tokenEndpoint) else {
Logger.shared.log("Invalid token endpoint URL", type: "Error")
Logger.shared.log("Invalid token endpoint URL", type: .error)
DispatchQueue.main.async {
NotificationCenter.default.post(name: authFailureNotification, object: nil, userInfo: ["error": "Invalid token endpoint URL"])
completion(false)
@ -63,14 +63,14 @@ class AniListToken {
let task = URLSession.shared.dataTask(with: request) { data, _, error in
DispatchQueue.main.async {
if let error {
Logger.shared.log("Error: \(error.localizedDescription)", type: "Error")
Logger.shared.log("Error: \(error.localizedDescription)", type: .error)
NotificationCenter.default.post(name: authFailureNotification, object: nil, userInfo: ["error": error.localizedDescription])
completion(false)
return
}
guard let data else {
Logger.shared.log("No data received", type: "Error")
Logger.shared.log("No data received", type: .error)
NotificationCenter.default.post(name: authFailureNotification, object: nil, userInfo: ["error": "No data received"])
completion(false)
return
@ -88,13 +88,13 @@ class AniListToken {
completion(success)
} else {
let errorMessage = (json["error"] as? String) ?? "Unexpected response"
Logger.shared.log("Authentication error: \(errorMessage)", type: "Error")
Logger.shared.log("Authentication error: \(errorMessage)", type: .error)
NotificationCenter.default.post(name: authFailureNotification, object: nil, userInfo: ["error": errorMessage])
completion(false)
}
}
} catch {
Logger.shared.log("Failed to parse JSON: \(error.localizedDescription)", type: "Error")
Logger.shared.log("Failed to parse JSON: \(error.localizedDescription)", type: .error)
NotificationCenter.default.post(name: authFailureNotification, object: nil, userInfo: ["error": "Failed to parse response: \(error.localizedDescription)"])
completion(false)
}

View file

@ -91,7 +91,7 @@ class AniListMutation {
if let data {
do {
_ = try JSONSerialization.jsonObject(with: data, options: [])
Logger.shared.log("Successfully updated anime progress", type: "Debug")
Logger.shared.log("Successfully updated anime progress", type: .debug)
completion(.success(()))
} catch {
completion(.failure(error))

View file

@ -22,13 +22,13 @@ class TraktLogin {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:]) { success in
if success {
Logger.shared.log("Safari opened successfully", type: "Debug")
Logger.shared.log("Safari opened successfully", type: .debug)
} else {
Logger.shared.log("Failed to open Safari", type: "Error")
Logger.shared.log("Failed to open Safari", type: .error)
}
}
} else {
Logger.shared.log("Cannot open URL", type: "Error")
Logger.shared.log("Cannot open URL", type: .error)
}
}
}

View file

@ -43,7 +43,7 @@ class TraktToken {
static func exchangeAuthorizationCodeForToken(code: String, completion: @escaping (Bool) -> Void) {
guard let url = URL(string: tokenEndpoint) else {
Logger.shared.log("Invalid token endpoint URL", type: "Error")
Logger.shared.log("Invalid token endpoint URL", type: .error)
handleFailure(error: "Invalid token endpoint URL", completion: completion)
return
}
@ -139,7 +139,7 @@ class TraktToken {
}
private static func handleFailure(error: String, completion: @escaping (Bool) -> Void) {
Logger.shared.log(error, type: "Error")
Logger.shared.log(error, type: .error)
NotificationCenter.default.post(name: authFailureNotification, object: nil, userInfo: ["error": error])
completion(false)
}

View file

@ -122,7 +122,7 @@ class TraktMutation {
return
}
Logger.shared.log("Successfully updated watch status on Trakt", type: "Debug")
Logger.shared.log("Successfully updated watch status on Trakt", type: .debug)
completion(.success(()))
}

View file

@ -23,7 +23,7 @@ class AnalyticsManager {
private let moduleManager = ModuleManager()
private init() {
print("[Info] Analytics initializer called")
Logger.shared.log("Analytics initializer called", type: .info)
}
// MARK: - Send Analytics Data
@ -38,12 +38,12 @@ class AnalyticsManager {
let analyticsEnabled = UserDefaults.standard.bool(forKey: "analyticsEnabled")
guard analyticsEnabled else {
Logger.shared.log("Analytics is disabled, skipping event: \(event)", type: "Debug")
Logger.shared.log("Analytics is disabled, skipping event: \(event)", type: .debug)
return
}
guard let selectedModule = getSelectedModule() else {
Logger.shared.log("No selected module found", type: "Debug")
Logger.shared.log("No selected module found", type: .debug)
return
}
@ -76,30 +76,30 @@ class AnalyticsManager {
do {
request.httpBody = try JSONSerialization.data(withJSONObject: data, options: [])
} catch {
Logger.shared.log("Failed to encode JSON: \(error.localizedDescription)", type: "Debug")
Logger.shared.log("Failed to encode JSON: \(error.localizedDescription)", type: .debug)
return
}
URLSession.shared.dataTask(with: request) { data, _, error in
if let error {
Logger.shared.log("Request failed: \(error.localizedDescription)", type: "Debug")
Logger.shared.log("Request failed: \(error.localizedDescription)", type: .debug)
return
}
guard let data else {
Logger.shared.log("No data received from server", type: "Debug")
Logger.shared.log("No data received from server", type: .debug)
return
}
do {
let decodedResponse = try JSONDecoder().decode(AnalyticsResponse.self, from: data)
if decodedResponse.status == "success" {
Logger.shared.log("Analytics saved: \(decodedResponse.event ?? "unknown event") at \(decodedResponse.timestamp ?? "unknown time")", type: "Debug")
Logger.shared.log("Analytics saved: \(decodedResponse.event ?? "unknown event") at \(decodedResponse.timestamp ?? "unknown time")", type: .debug)
} else {
Logger.shared.log("Server error: \(decodedResponse.message)", type: "Debug")
Logger.shared.log("Server error: \(decodedResponse.message)", type: .debug)
}
} catch {
Logger.shared.log("Failed to decode response: \(error.localizedDescription)", type: "Debug")
Logger.shared.log("Failed to decode response: \(error.localizedDescription)", type: .debug)
}
}.resume()
}

View file

@ -58,7 +58,7 @@ class DownloadManager: NSObject, ObservableObject {
localPlaybackURL = localURL
}
} catch {
print("Error loading local content: \(error)")
Logger.shared.log("Error loading local content: \(error)", type: .error)
}
}
}
@ -75,7 +75,7 @@ extension DownloadManager: AVAssetDownloadDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let error else { return }
print("Download error: \(error.localizedDescription)")
Logger.shared.log("Download error: \(error.localizedDescription)", type: .error)
activeDownloadTasks.removeValue(forKey: task)
}

View file

@ -12,7 +12,7 @@ class DropManager {
static let shared = DropManager()
private init() {
print("[Info] Drops initialized")
Logger.shared.log("Drops initialized", type: .info)
}
func showDrop(title: String, subtitle: String, duration: TimeInterval, icon: UIImage?) {

View file

@ -12,19 +12,19 @@ extension JSContext {
let consoleObject = JSValue(newObjectIn: self)
let consoleLogFunction: @convention(block) (String) -> Void = { message in
Logger.shared.log(message, type: "Debug")
Logger.shared.log(message, type: .debug)
}
consoleObject?.setObject(consoleLogFunction, forKeyedSubscript: "log" as NSString)
let consoleErrorFunction: @convention(block) (String) -> Void = { message in
Logger.shared.log(message, type: "Error")
Logger.shared.log(message, type: .error)
}
consoleObject?.setObject(consoleErrorFunction, forKeyedSubscript: "error" as NSString)
self.setObject(consoleObject, forKeyedSubscript: "console" as NSString)
let logFunction: @convention(block) (String) -> Void = { message in
Logger.shared.log("JavaScript log: \(message)", type: "Debug")
Logger.shared.log("JavaScript log: \(message)", type: .debug)
}
self.setObject(logFunction, forKeyedSubscript: "log" as NSString)
}
@ -32,7 +32,7 @@ extension JSContext {
func setupNativeFetch() {
let fetchNativeFunction: @convention(block) (String, [String: String]?, JSValue, JSValue) -> Void = { urlString, headers, resolve, reject in
guard let url = URL(string: urlString) else {
Logger.shared.log("Invalid URL", type: "Error")
Logger.shared.log("Invalid URL", type: .error)
reject.call(withArguments: ["Invalid URL"])
return
}
@ -44,19 +44,19 @@ extension JSContext {
}
let task = URLSession.custom.dataTask(with: request) { data, _, error in
if let error {
Logger.shared.log("Network error in fetchNativeFunction: \(error.localizedDescription)", type: "Error")
Logger.shared.log("Network error in fetchNativeFunction: \(error.localizedDescription)", type: .error)
reject.call(withArguments: [error.localizedDescription])
return
}
guard let data else {
Logger.shared.log("No data in response", type: "Error")
Logger.shared.log("No data in response", type: .error)
reject.call(withArguments: ["No data"])
return
}
if let text = String(data: data, encoding: .utf8) {
resolve.call(withArguments: [text])
} else {
Logger.shared.log("Unable to decode data to text", type: "Error")
Logger.shared.log("Unable to decode data to text", type: .error)
reject.call(withArguments: ["Unable to decode data"])
}
}
@ -77,7 +77,7 @@ extension JSContext {
func setupFetchV2() {
let fetchV2NativeFunction: @convention(block) (String, [String: String]?, String?, String?, ObjCBool, JSValue, JSValue) -> Void = { urlString, headers, method, body, redirect, resolve, reject in
guard let url = URL(string: urlString) else {
Logger.shared.log("Invalid URL", type: "Error")
Logger.shared.log("Invalid URL", type: .error)
reject.call(withArguments: ["Invalid URL"])
return
}
@ -86,10 +86,10 @@ extension JSContext {
var request = URLRequest(url: url)
request.httpMethod = httpMethod
Logger.shared.log("FetchV2 Request: URL=\(url), Method=\(httpMethod), Body=\(body ?? "nil")", type: "Debug")
Logger.shared.log("FetchV2 Request: URL=\(url), Method=\(httpMethod), Body=\(body ?? "nil")", type: .debug)
if httpMethod == "GET", let body, !body.isEmpty, body != "null", body != "undefined" {
Logger.shared.log("GET request must not have a body", type: "Error")
Logger.shared.log("GET request must not have a body", type: .error)
reject.call(withArguments: ["GET request must not have a body"])
return
}
@ -103,16 +103,16 @@ extension JSContext {
request.setValue(value, forHTTPHeaderField: key)
}
}
Logger.shared.log("Redirect value is \(redirect.boolValue)", type: "Error")
Logger.shared.log("Redirect value is \(redirect.boolValue)", type: .error)
let task = URLSession.fetchData(allowRedirects: redirect.boolValue).downloadTask(with: request) { tempFileURL, response, error in
if let error {
Logger.shared.log("Network error in fetchV2NativeFunction: \(error.localizedDescription)", type: "Error")
Logger.shared.log("Network error in fetchV2NativeFunction: \(error.localizedDescription)", type: .error)
reject.call(withArguments: [error.localizedDescription])
return
}
guard let tempFileURL else {
Logger.shared.log("No data in response", type: "Error")
Logger.shared.log("No data in response", type: .error)
reject.call(withArguments: ["No data"])
return
}
@ -127,7 +127,7 @@ extension JSContext {
let data = try Data(contentsOf: tempFileURL)
if data.count > 10_000_000 {
Logger.shared.log("Response exceeds maximum size", type: "Error")
Logger.shared.log("Response exceeds maximum size", type: .error)
reject.call(withArguments: ["Response exceeds maximum size"])
return
}
@ -137,11 +137,11 @@ extension JSContext {
resolve.call(withArguments: [responseDict])
} else {
// rather than reject -> resolve with empty body as user can utilise reponse headers.
Logger.shared.log("Unable to decode data to text", type: "Error")
Logger.shared.log("Unable to decode data to text", type: .error)
resolve.call(withArguments: [responseDict])
}
} catch {
Logger.shared.log("Error reading downloaded file: \(error.localizedDescription)", type: "Error")
Logger.shared.log("Error reading downloaded file: \(error.localizedDescription)", type: .error)
reject.call(withArguments: ["Error reading downloaded file"])
}
}
@ -190,7 +190,7 @@ extension JSContext {
func setupBase64Functions() {
let btoaFunction: @convention(block) (String) -> String? = { data in
guard let data = data.data(using: .utf8) else {
Logger.shared.log("btoa: Failed to encode input as UTF-8", type: "Error")
Logger.shared.log("btoa: Failed to encode input as UTF-8", type: .error)
return nil
}
return data.base64EncodedString()
@ -198,7 +198,7 @@ extension JSContext {
let atobFunction: @convention(block) (String) -> String? = { base64String in
guard let data = Data(base64Encoded: base64String) else {
Logger.shared.log("atob: Invalid base64 input", type: "Error")
Logger.shared.log("atob: Invalid base64 input", type: .error)
return nil
}

View file

@ -18,13 +18,13 @@ extension JSController {
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
}
@ -32,7 +32,7 @@ extension JSController {
var resultItems: [MediaItem] = []
var episodeLinks: [EpisodeLink] = []
Logger.shared.log(html, type: "HTMLStrings")
Logger.shared.log(html, type: .html)
if let parseFunction = self.context.objectForKeyedSubscript("extractDetails"),
let results = parseFunction.call(withArguments: [html]).toArray() as? [[String: String]] {
resultItems = results.map { item in
@ -43,7 +43,7 @@ extension JSController {
)
}
} else {
Logger.shared.log("Failed to parse results", type: "Error")
Logger.shared.log("Failed to parse results", type: .error)
}
if let fetchEpisodesFunction = self.context.objectForKeyedSubscript("extractEpisodes"),
@ -68,19 +68,19 @@ extension JSController {
}
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 extractDetailsFunction = context.objectForKeyedSubscript("extractDetails") else {
Logger.shared.log("No JavaScript function extractDetails found", type: "Error")
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")
Logger.shared.log("No JavaScript function extractEpisodes found", type: .error)
completion([], [])
return
}
@ -93,13 +93,13 @@ extension JSController {
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")
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")
Logger.shared.log(result.toString(), type: .debug)
if let jsonOfDetails = result.toString(),
let dataDetails = jsonOfDetails.data(using: .utf8) {
do {
@ -112,19 +112,19 @@ extension JSController {
)
}
} else {
Logger.shared.log("Failed to parse JSON of extractDetails", type: "Error")
Logger.shared.log("Failed to parse JSON of extractDetails", type: .error)
}
} catch {
Logger.shared.log("JSON parsing error of extract details: \(error)", type: "Error")
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")
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")
Logger.shared.log("Promise rejected of extractDetails: \(String(describing: error.toString()))", type: .error)
dispatchGroup.leave()
}
@ -137,13 +137,13 @@ extension JSController {
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")
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")
Logger.shared.log(result.toString(), type: .debug)
if let jsonOfEpisodes = result.toString(),
let dataEpisodes = jsonOfEpisodes.data(using: .utf8) {
do {
@ -155,19 +155,19 @@ extension JSController {
)
}
} else {
Logger.shared.log("Failed to parse JSON of extractEpisodes", type: "Error")
Logger.shared.log("Failed to parse JSON of extractEpisodes", type: .error)
}
} catch {
Logger.shared.log("JSON parsing error of extractEpisodes: \(error)", type: "Error")
Logger.shared.log("JSON parsing error of extractEpisodes: \(error)", type: .error)
}
} else {
Logger.shared.log("Result is not a string of extractEpisodes", type: "Error")
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")
Logger.shared.log("Promise rejected of extractEpisodes: \(String(describing: error.toString()))", type: .error)
dispatchGroup.leave()
}

View file

@ -85,7 +85,7 @@ extension JSController {
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")
Logger.shared.log("Missing or invalid data in search result item: \(item)", type: .error)
return nil
}
return SearchItem(title: title, imageUrl: imageUrl, href: href)

View file

@ -20,18 +20,18 @@ extension JSController {
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")
Logger.shared.log(html, type: .html)
if let parseFunction = self.context.objectForKeyedSubscript("searchResults"),
let results = parseFunction.call(withArguments: [html]).toArray() as? [[String: String]] {
let resultItems = results.map { item in
@ -45,7 +45,7 @@ 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()
@ -53,26 +53,26 @@ extension JSController {
func fetchJsSearchResults(keyword: String, module: ScrapingModule, completion: @escaping ([SearchItem]) -> Void) {
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")
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")
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")
Logger.shared.log(result.toString(), type: .html)
if let jsonString = result.toString(),
let data = jsonString.data(using: .utf8) {
do {
@ -81,7 +81,7 @@ extension JSController {
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")
Logger.shared.log("Missing or invalid data in search result item: \(item)", type: .error)
return nil
}
return SearchItem(title: title, imageUrl: imageUrl, href: href)
@ -91,19 +91,19 @@ extension JSController {
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([])
}
@ -111,7 +111,7 @@ extension JSController {
}
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([])
}

View file

@ -18,18 +18,18 @@ extension JSController {
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((nil, nil)) }
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((nil, nil)) }
return
}
Logger.shared.log(html, type: "HTMLStrings")
Logger.shared.log(html, type: .html)
if let parseFunction = self.context.objectForKeyedSubscript("extractStreamUrl"),
let resultString = parseFunction.call(withArguments: [html]).toString() {
if let data = resultString.data(using: .utf8) {
@ -40,21 +40,21 @@ extension JSController {
if let streamsArray = json["streams"] as? [String] {
streamUrls = streamsArray
Logger.shared.log("Found \(streamsArray.count) streams", type: "Stream")
Logger.shared.log("Found \(streamsArray.count) streams", type: .debug)
} else if let streamUrl = json["stream"] as? String {
streamUrls = [streamUrl]
Logger.shared.log("Found single stream", type: "Stream")
Logger.shared.log("Found single stream", type: .debug)
}
if let subsArray = json["subtitles"] as? [String] {
subtitleUrls = subsArray
Logger.shared.log("Found \(subsArray.count) subtitle tracks", type: "Stream")
Logger.shared.log("Found \(subsArray.count) subtitle tracks", type: .debug)
} else if let subtitleUrl = json["subtitles"] as? String {
subtitleUrls = [subtitleUrl]
Logger.shared.log("Found single subtitle track", type: "Stream")
Logger.shared.log("Found single subtitle track", type: .debug)
}
Logger.shared.log("Starting stream with \(streamUrls?.count ?? 0) sources and \(subtitleUrls?.count ?? 0) subtitles", type: "Stream")
Logger.shared.log("Starting stream with \(streamUrls?.count ?? 0) sources and \(subtitleUrls?.count ?? 0) subtitles", type: .debug)
DispatchQueue.main.async {
completion((streamUrls, subtitleUrls))
}
@ -62,17 +62,17 @@ extension JSController {
}
if let streamsArray = try? JSONSerialization.jsonObject(with: data, options: []) as? [String] {
Logger.shared.log("Starting multi-stream with \(streamsArray.count) sources", type: "Stream")
Logger.shared.log("Starting multi-stream with \(streamsArray.count) sources", type: .debug)
DispatchQueue.main.async { completion((streamsArray, nil)) }
return
}
}
}
Logger.shared.log("Starting stream from: \(resultString)", type: "Stream")
Logger.shared.log("Starting stream from: \(resultString)", type: .debug)
DispatchQueue.main.async { completion(([resultString], nil)) }
} else {
Logger.shared.log("Failed to extract stream URL", type: "Error")
Logger.shared.log("Failed to extract stream URL", type: .error)
DispatchQueue.main.async { completion((nil, nil)) }
}
}.resume()
@ -80,20 +80,20 @@ extension JSController {
func fetchStreamUrlJS(episodeUrl: String, softsub: Bool = false, module: ScrapingModule, completion: @escaping ((streams: [String]?, subtitles: [String]?)) -> Void) {
if let exception = context.exception {
Logger.shared.log("JavaScript exception: \(exception)", type: "Error")
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")
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")
Logger.shared.log("extractStreamUrl did not return a Promise", type: .error)
completion((nil, nil))
return
}
@ -110,21 +110,21 @@ extension JSController {
if let streamsArray = json["streams"] as? [String] {
streamUrls = streamsArray
Logger.shared.log("Found \(streamsArray.count) streams", type: "Stream")
Logger.shared.log("Found \(streamsArray.count) streams", type: .debug)
} else if let streamUrl = json["stream"] as? String {
streamUrls = [streamUrl]
Logger.shared.log("Found single stream", type: "Stream")
Logger.shared.log("Found single stream", type: .debug)
}
if let subsArray = json["subtitles"] as? [String] {
subtitleUrls = subsArray
Logger.shared.log("Found \(subsArray.count) subtitle tracks", type: "Stream")
Logger.shared.log("Found \(subsArray.count) subtitle tracks", type: .debug)
} else if let subtitleUrl = json["subtitles"] as? String {
subtitleUrls = [subtitleUrl]
Logger.shared.log("Found single subtitle track", type: "Stream")
Logger.shared.log("Found single subtitle track", type: .debug)
}
Logger.shared.log("Starting stream with \(streamUrls?.count ?? 0) sources and \(subtitleUrls?.count ?? 0) subtitles", type: "Stream")
Logger.shared.log("Starting stream with \(streamUrls?.count ?? 0) sources and \(subtitleUrls?.count ?? 0) subtitles", type: .debug)
DispatchQueue.main.async {
completion((streamUrls, subtitleUrls))
}
@ -132,7 +132,7 @@ extension JSController {
}
if let streamsArray = try? JSONSerialization.jsonObject(with: data, options: []) as? [String] {
Logger.shared.log("Starting multi-stream with \(streamsArray.count) sources", type: "Stream")
Logger.shared.log("Starting multi-stream with \(streamsArray.count) sources", type: .debug)
DispatchQueue.main.async { completion((streamsArray, nil)) }
return
}
@ -140,14 +140,14 @@ extension JSController {
}
let streamUrl = result.toString()
Logger.shared.log("Starting stream from: \(streamUrl ?? "nil")", type: "Stream")
Logger.shared.log("Starting stream from: \(streamUrl ?? "nil")", type: .debug)
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")
Logger.shared.log("Promise rejected: \(String(describing: error.toString()))", type: .error)
DispatchQueue.main.async {
completion((nil, nil))
}
@ -166,33 +166,33 @@ extension JSController {
guard let self else { return }
if let error {
Logger.shared.log("URLSession error: \(error.localizedDescription)", type: "Error")
Logger.shared.log("URLSession error: \(error.localizedDescription)", type: .error)
DispatchQueue.main.async { completion((nil, nil)) }
return
}
guard let data, let htmlString = String(data: data, encoding: .utf8) else {
Logger.shared.log("Failed to fetch HTML data", type: "Error")
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")
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")
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")
Logger.shared.log("extractStreamUrl did not return a Promise", type: .error)
completion((nil, nil))
return
}
@ -209,21 +209,21 @@ extension JSController {
if let streamsArray = json["streams"] as? [String] {
streamUrls = streamsArray
Logger.shared.log("Found \(streamsArray.count) streams", type: "Stream")
Logger.shared.log("Found \(streamsArray.count) streams", type: .debug)
} else if let streamUrl = json["stream"] as? String {
streamUrls = [streamUrl]
Logger.shared.log("Found single stream", type: "Stream")
Logger.shared.log("Found single stream", type: .debug)
}
if let subsArray = json["subtitles"] as? [String] {
subtitleUrls = subsArray
Logger.shared.log("Found \(subsArray.count) subtitle tracks", type: "Stream")
Logger.shared.log("Found \(subsArray.count) subtitle tracks", type: .debug)
} else if let subtitleUrl = json["subtitles"] as? String {
subtitleUrls = [subtitleUrl]
Logger.shared.log("Found single subtitle track", type: "Stream")
Logger.shared.log("Found single subtitle track", type: .debug)
}
Logger.shared.log("Starting stream with \(streamUrls?.count ?? 0) sources and \(subtitleUrls?.count ?? 0) subtitles", type: "Stream")
Logger.shared.log("Starting stream with \(streamUrls?.count ?? 0) sources and \(subtitleUrls?.count ?? 0) subtitles", type: .debug)
DispatchQueue.main.async {
completion((streamUrls, subtitleUrls))
}
@ -231,7 +231,7 @@ extension JSController {
}
if let streamsArray = try? JSONSerialization.jsonObject(with: data, options: []) as? [String] {
Logger.shared.log("Starting multi-stream with \(streamsArray.count) sources", type: "Stream")
Logger.shared.log("Starting multi-stream with \(streamsArray.count) sources", type: .debug)
DispatchQueue.main.async { completion((streamsArray, nil)) }
return
}
@ -239,14 +239,14 @@ extension JSController {
}
let streamUrl = result.toString()
Logger.shared.log("Starting stream from: \(streamUrl ?? "nil")", type: "Stream")
Logger.shared.log("Starting stream from: \(streamUrl ?? "nil")", type: .debug)
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")
Logger.shared.log("Promise rejected: \(String(describing: error.toString()))", type: .error)
DispatchQueue.main.async {
completion((nil, nil))
}

View file

@ -24,7 +24,7 @@ class JSController: ObservableObject {
setupContext()
context.evaluateScript(script)
if let exception = context.exception {
Logger.shared.log("Error loading script: \(exception)", type: "Error")
Logger.shared.log("Error loading script: \(exception)", type: .error)
}
}
}

View file

@ -25,10 +25,19 @@ class Logger {
logFileURL = documentDirectory.appendingPathComponent("logs.txt")
}
func log(_ message: String, type: String = "General") {
guard logFilterViewModel.isFilterEnabled(for: type) else { return }
enum LogLevel: String {
case info = "Info"
case warning = "Warning"
case error = "Error"
case general = "General"
case debug = "Debug"
case html = "HTML"
}
let entry = LogEntry(message: message, type: type, timestamp: Date())
func log(_ message: String, type: LogLevel = .general) {
guard logFilterViewModel.isFilterEnabled(for: type.rawValue) else { return }
let entry = LogEntry(message: message, type: type.rawValue, timestamp: Date())
logs.append(entry)
saveLogToFile(entry)

View file

@ -106,11 +106,11 @@ struct VolumeSlider<T: BinaryFloatingPoint>: View {
switch p {
case muteThreshold:
return "speaker.slash.fill"
case muteThreshold..<lowThreshold:
case muteThreshold ..< lowThreshold:
return "speaker.fill"
case lowThreshold..<midThreshold:
case lowThreshold ..< midThreshold:
return "speaker.wave.1.fill"
case midThreshold..<highThreshold:
case midThreshold ..< highThreshold:
return "speaker.wave.2.fill"
default:
return "speaker.wave.3.fill"

View file

@ -38,8 +38,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
var holdGesture: UILongPressGestureRecognizer?
var isPlaying = true
var currentTimeVal: Double = 0.0
var duration: Double = 0.0
var currentTimeVal = 0.0
var duration = 0.0
var isVideoLoaded = false
private var isHoldPauseEnabled: Bool {
@ -68,13 +68,13 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
private var currentMenuButtonTrailing: NSLayoutConstraint!
var subtitleForegroundColor: UIColor = .white
var subtitleBackgroundEnabled: Bool = true
var subtitleFontSize: Double = 20.0
var subtitleShadowRadius: Double = 1.0
var subtitleBackgroundEnabled = true
var subtitleFontSize = 20.0
var subtitleShadowRadius = 1.0
var subtitlesLoader = VTTSubtitlesLoader()
var subtitleStackView: UIStackView!
var subtitleLabels: [UILabel] = []
var subtitlesEnabled: Bool = true {
var subtitlesEnabled = true {
didSet {
subtitleStackView.isHidden = !subtitlesEnabled
}
@ -97,7 +97,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
var qualityButton: UIButton!
var holdSpeedIndicator: UIButton!
var isHLSStream: Bool = false
var isHLSStream = false
var qualities: [(String, String)] = []
var currentQualityURL: URL?
var baseM3U8URL: URL?
@ -165,10 +165,10 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
private var audioSession = AVAudioSession.sharedInstance()
private var hiddenVolumeView = MPVolumeView(frame: .zero)
private var systemVolumeSlider: UISlider?
private var volumeValue: Double = 0.0
private var volumeValue = 0.0
private var volumeViewModel = VolumeViewModel()
var volumeSliderHostingView: UIView?
private var subtitleDelay: Double = 0.0
private var subtitleDelay = 0.0
init(module: ScrapingModule,
continueWatchingManager: ContinueWatchingManager,
@ -180,7 +180,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
subtitlesURL: String?,
aniListID: Int,
episodeImageUrl: String) {
self.module = module
self.continueWatchingManager = continueWatchingManager
self.streamURL = urlString
@ -215,6 +214,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@ -255,7 +255,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
self?.fetchSkipTimes(type: "op")
self?.fetchSkipTimes(type: "ed")
case .failure(let error):
Logger.shared.log("Unable to fetch MAL ID: \(error)", type: "Error")
Logger.shared.log("Unable to fetch MAL ID: \(error)", type: .error)
}
}
@ -272,7 +272,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
do {
try audioSession.setActive(true)
} catch {
Logger.shared.log("Error activating audio session: \(error)", type: "Debug")
Logger.shared.log("Error activating audio session: \(error)", type: .debug)
}
volumeViewModel.value = Double(audioSession.outputVolume)
@ -281,7 +281,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
guard let newVol = change.newValue else { return }
DispatchQueue.main.async {
self?.volumeViewModel.value = Double(newVol)
Logger.shared.log("Hardware volume changed, new value: \(newVol)", type: "Debug")
Logger.shared.log("Hardware volume changed, new value: \(newVol)", type: .debug)
}
}
@ -329,7 +329,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard let marqueeLabel = marqueeLabel else {
guard let marqueeLabel else {
return
}
@ -386,12 +386,12 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
@objc
private func playerItemDidChange() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if self.qualityButton.isHidden && self.isHLSStream {
self.qualityButton.isHidden = false
self.qualityButton.menu = self.qualitySelectionMenu()
guard let self else { return }
if qualityButton.isHidden && isHLSStream {
qualityButton.isHidden = false
qualityButton.menu = qualitySelectionMenu()
self.updateMenuButtonConstraints()
updateMenuButtonConstraints()
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut) {
self.view.layoutIfNeeded()
@ -546,15 +546,15 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
toleranceBefore: .zero,
toleranceAfter: .zero
) { [weak self] _ in
guard let self = self else { return }
guard let self else { return }
let final = self.player.currentTime().seconds
self.sliderViewModel.sliderValue = final
self.currentTimeVal = final
self.isSliderEditing = false
let final = player.currentTime().seconds
sliderViewModel.sliderValue = final
currentTimeVal = final
isSliderEditing = false
if self.wasPlayingBeforeSeek {
self.player.playImmediately(atRate: self.originalRate)
if wasPlayingBeforeSeek {
player.playImmediately(atRate: originalRate)
}
}
}
@ -726,7 +726,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
subtitleStackView.distribution = .fill
subtitleStackView.spacing = 2
if let subtitleStackView = subtitleStackView {
if let subtitleStackView {
view.addSubview(subtitleStackView)
subtitleStackView.translatesAutoresizingMaskIntoConstraints = false
@ -749,7 +749,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
subtitleBottomToSafeAreaConstraint?.isActive = true
}
for _ in 0..<2 {
for _ in 0 ..< 2 {
let label = UILabel()
label.textAlignment = .center
label.numberOfLines = 0
@ -866,7 +866,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
holdSpeedIndicator.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .bold)
holdSpeedIndicator.setImage(image, for: .normal)
holdSpeedIndicator.backgroundColor = UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 0.8)
holdSpeedIndicator.backgroundColor = UIColor(red: 51 / 255.0, green: 51 / 255.0, blue: 51 / 255.0, alpha: 0.8)
holdSpeedIndicator.tintColor = .white
holdSpeedIndicator.setTitleColor(.white, for: .normal)
holdSpeedIndicator.layer.cornerRadius = 21
@ -975,7 +975,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
self.player.pause()
} else {
let target = CMTime(seconds: self.sliderViewModel.sliderValue,
preferredTimescale: 600)
self.player.seek(
@ -983,15 +982,15 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
toleranceBefore: .zero,
toleranceAfter: .zero
) { [weak self] _ in
guard let self = self else { return }
guard let self else { return }
let final = self.player.currentTime().seconds
self.sliderViewModel.sliderValue = final
self.currentTimeVal = final
self.isSliderEditing = false
let final = player.currentTime().seconds
sliderViewModel.sliderValue = final
currentTimeVal = final
isSliderEditing = false
if self.wasPlayingBeforeSeek {
self.player.playImmediately(atRate: self.originalRate)
if wasPlayingBeforeSeek {
player.playImmediately(atRate: originalRate)
}
}
}
@ -1038,7 +1037,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
skipIntroButton.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .bold)
skipIntroButton.setImage(introImage, for: .normal)
skipIntroButton.backgroundColor = UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 0.8)
skipIntroButton.backgroundColor = UIColor(red: 51 / 255.0, green: 51 / 255.0, blue: 51 / 255.0, alpha: 0.8)
if #available(iOS 15.0, *) {
var config = UIButton.Configuration.filled()
@ -1083,7 +1082,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
skipOutroButton.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .bold)
skipOutroButton.setImage(outroImage, for: .normal)
skipOutroButton.backgroundColor = UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 0.8)
skipOutroButton.backgroundColor = UIColor(red: 51 / 255.0, green: 51 / 255.0, blue: 51 / 255.0, alpha: 0.8)
if #available(iOS 15.0, *) {
var config = UIButton.Configuration.filled()
@ -1174,7 +1173,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
menuButton.setImage(image, for: .normal)
menuButton.tintColor = .white
if let subtitlesURL = subtitlesURL, !subtitlesURL.isEmpty {
if let subtitlesURL, !subtitlesURL.isEmpty {
menuButton.showsMenuAsPrimaryAction = true
menuButton.menu = buildOptionsMenu()
} else {
@ -1265,7 +1264,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
skip85Button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .bold)
skip85Button.setImage(image, for: .normal)
skip85Button.backgroundColor = UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 0.8)
skip85Button.backgroundColor = UIColor(red: 51 / 255.0, green: 51 / 255.0, blue: 51 / 255.0, alpha: 0.8)
if #available(iOS 15.0, *) {
var config = UIButton.Configuration.filled()
@ -1354,51 +1353,51 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval,
queue: .main) { [weak self] time in
guard let self = self,
let currentItem = self.player.currentItem,
guard let self,
let currentItem = player.currentItem,
currentItem.duration.seconds.isFinite else { return }
let currentDuration = currentItem.duration.seconds
if currentDuration.isNaN || currentDuration <= 0 { return }
self.currentTimeVal = time.seconds
self.duration = currentDuration
self.updateSegments()
currentTimeVal = time.seconds
duration = currentDuration
updateSegments()
if !self.isSliderEditing {
self.sliderViewModel.sliderValue = max(0, min(self.currentTimeVal, self.duration))
if !isSliderEditing {
sliderViewModel.sliderValue = max(0, min(currentTimeVal, duration))
}
self.updateSkipButtonsVisibility()
updateSkipButtonsVisibility()
UserDefaults.standard.set(self.currentTimeVal, forKey: "lastPlayedTime_\(self.fullUrl)")
UserDefaults.standard.set(self.duration, forKey: "totalTime_\(self.fullUrl)")
UserDefaults.standard.set(currentTimeVal, forKey: "lastPlayedTime_\(fullUrl)")
UserDefaults.standard.set(duration, forKey: "totalTime_\(fullUrl)")
if subtitlesEnabled {
let adjustedTime = self.currentTimeVal - self.subtitleDelay
let cues = self.subtitlesLoader.cues.filter { adjustedTime >= $0.startTime && adjustedTime <= $0.endTime }
let adjustedTime = currentTimeVal - subtitleDelay
let cues = subtitlesLoader.cues.filter { adjustedTime >= $0.startTime && adjustedTime <= $0.endTime }
if cues.isEmpty {
self.subtitleLabels[0].text = cues[0].text.strippedHTML
self.subtitleLabels[0].isHidden = false
subtitleLabels[0].text = cues[0].text.strippedHTML
subtitleLabels[0].isHidden = false
} else {
self.subtitleLabels[0].text = ""
self.subtitleLabels[0].isHidden = !self.subtitlesEnabled
subtitleLabels[0].text = ""
subtitleLabels[0].isHidden = !subtitlesEnabled
}
if cues.count > 1 {
self.subtitleLabels[1].text = cues[1].text.strippedHTML
self.subtitleLabels[1].isHidden = false
subtitleLabels[1].text = cues[1].text.strippedHTML
subtitleLabels[1].isHidden = false
} else {
self.subtitleLabels[1].text = ""
self.subtitleLabels[1].isHidden = true
subtitleLabels[1].text = ""
subtitleLabels[1].isHidden = true
}
} else {
self.subtitleLabels[0].text = ""
self.subtitleLabels[0].isHidden = true
self.subtitleLabels[1].text = ""
self.subtitleLabels[1].isHidden = true
subtitleLabels[0].text = ""
subtitleLabels[0].isHidden = true
subtitleLabels[1].text = ""
subtitleLabels[1].isHidden = true
}
let segmentsColor = self.getSegmentsColor()
let segmentsColor = getSegmentsColor()
DispatchQueue.main.async {
if let currentItem = self.player.currentItem, currentItem.duration.seconds > 0 {
@ -1457,15 +1456,15 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
toleranceBefore: .zero,
toleranceAfter: .zero
) { [weak self] _ in
guard let self = self else { return }
guard let self else { return }
let final = self.player.currentTime().seconds
self.sliderViewModel.sliderValue = final
self.currentTimeVal = final
self.isSliderEditing = false
let final = player.currentTime().seconds
sliderViewModel.sliderValue = final
currentTimeVal = final
isSliderEditing = false
if self.wasPlayingBeforeSeek {
self.player.playImmediately(atRate: self.originalRate)
if wasPlayingBeforeSeek {
player.playImmediately(atRate: originalRate)
}
}
}
@ -1497,8 +1496,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
func startUpdateTimer() {
updateTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
guard let self = self else { return }
self.currentTimeVal = self.player.currentTime().seconds
guard let self else { return }
currentTimeVal = player.currentTime().seconds
}
}
@ -1525,7 +1524,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
dimButton.alpha = 1.0
dimButtonTimer?.invalidate()
dimButtonTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { [weak self] _ in
guard let self = self else { return }
guard let self else { return }
UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut]) {
self.dimButton.alpha = 0
}
@ -1589,7 +1588,10 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
let finalSkip = skipValue > 0 ? skipValue : 10
currentTimeVal = min(currentTimeVal + finalSkip, duration)
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] _ in
guard self != nil else { return } }
guard self != nil else {
return
}
}
animateButtonRotation(forwardButton)
}
@ -1699,33 +1701,32 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
private func tryAniListUpdate() {
let aniListMutation = AniListMutation()
aniListMutation.updateAnimeProgress(animeId: self.aniListID, episodeNumber: self.episodeNumber) { [weak self] result in
guard let self = self else { return }
guard let self else { return }
switch result {
case .success:
self.aniListUpdatedSuccessfully = true
Logger.shared.log("Successfully updated AniList progress for episode \(self.episodeNumber)", type: "General")
aniListUpdatedSuccessfully = true
Logger.shared.log("Successfully updated AniList progress for episode \(episodeNumber)", type: .general)
case .failure(let error):
let errorString = error.localizedDescription.lowercased()
Logger.shared.log("AniList progress update failed: \(errorString)", type: "Error")
Logger.shared.log("AniList progress update failed: \(errorString)", type: .error)
if errorString.contains("access token not found") {
Logger.shared.log("AniList update will NOT retry due to missing token.", type: "Error")
self.aniListUpdateImpossible = true
Logger.shared.log("AniList update will NOT retry due to missing token.", type: .error)
aniListUpdateImpossible = true
} else {
if self.aniListRetryCount < self.aniListMaxRetries {
self.aniListRetryCount += 1
if aniListRetryCount < aniListMaxRetries {
aniListRetryCount += 1
let delaySeconds = 5.0
Logger.shared.log("AniList update will retry in \(delaySeconds)s (attempt \(self.aniListRetryCount)).", type: "Debug")
Logger.shared.log("AniList update will retry in \(delaySeconds)s (attempt \(aniListRetryCount)).", type: .debug)
DispatchQueue.main.asyncAfter(deadline: .now() + delaySeconds) {
self.tryAniListUpdate()
}
} else {
Logger.shared.log("AniList update reached max retries. No more attempts.", type: "Error")
Logger.shared.log("AniList update reached max retries. No more attempts.", type: .error)
}
}
}
@ -1763,7 +1764,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
forHTTPHeaderField: "User-Agent")
URLSession.shared.dataTask(with: request) { [weak self] data, _, _ in
guard let self = self,
guard let self,
let data,
let content = String(data: data, encoding: .utf8) else {
Logger.shared.log("Failed to load m3u8 file")
@ -1782,8 +1783,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
func getQualityName(for height: Int) -> String {
switch height {
case 1080...: return "\(height)p (FHD)"
case 720..<1080: return "\(height)p (HD)"
case 480..<720: return "\(height)p (SD)"
case 720 ..< 1080: return "\(height)p (HD)"
case 480 ..< 720: return "\(height)p (SD)"
default: return "\(height)p"
}
}
@ -1793,17 +1794,15 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
if let resolutionRange = line.range(of: "RESOLUTION="),
let resolutionEndRange = line[resolutionRange.upperBound...].range(of: ",")
?? line[resolutionRange.upperBound...].range(of: "\n") {
let resolutionPart = String(line[resolutionRange.upperBound..<resolutionEndRange.lowerBound])
let resolutionPart = String(line[resolutionRange.upperBound ..< resolutionEndRange.lowerBound])
if let heightStr = resolutionPart.components(separatedBy: "x").last,
let height = Int(heightStr) {
let nextLine = lines[index + 1].trimmingCharacters(in: .whitespacesAndNewlines)
let qualityName = getQualityName(for: height)
var qualityURL = nextLine
if !nextLine.hasPrefix("http") && nextLine.contains(".m3u8") {
if let baseURL = self.baseM3U8URL {
if let baseURL = baseM3U8URL {
let baseURLString = baseURL.deletingLastPathComponent().absoluteString
qualityURL = URL(string: nextLine, relativeTo: baseURL)?.absoluteString
?? baseURLString + "/" + nextLine
@ -1876,7 +1875,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
if isHLSStream {
if qualities.isEmpty {
let loadingAction = UIAction(title: "Loading qualities...", attributes: .disabled) { _ in }
let loadingAction = UIAction(title: "Loading qualities...", attributes: .disabled) { _ in
Logger.shared.log("Loading State Button clicked", type: .info)
}
menuItems.append(loadingAction)
} else {
var menuTitle = "Video Quality"
@ -1901,7 +1902,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
return UIMenu(title: menuTitle, children: menuItems)
}
} else {
let unavailableAction = UIAction(title: "Quality selection unavailable", attributes: .disabled) { _ in }
let unavailableAction = UIAction(title: "Quality selection unavailable", attributes: .disabled) { _ in
Logger.shared.log("Disabled State Button clicked", type: .info)
}
menuItems.append(unavailableAction)
}
@ -1917,15 +1920,15 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
currentQualityURL = url
parseM3U8(url: url) { [weak self] in
guard let self = self else { return }
guard let self else { return }
if let last = UserDefaults.standard.string(forKey: "lastSelectedQuality"),
self.qualities.contains(where: { $0.1 == last }) {
self.switchToQuality(urlString: last)
qualities.contains(where: { $0.1 == last }) {
switchToQuality(urlString: last)
}
self.qualityButton.isHidden = false
self.qualityButton.menu = self.qualitySelectionMenu()
self.updateMenuButtonConstraints()
qualityButton.isHidden = false
qualityButton.menu = qualitySelectionMenu()
updateMenuButtonConstraints()
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut) {
self.view.layoutIfNeeded()
}
@ -1942,8 +1945,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
if let subURL = subtitlesURL, !subURL.isEmpty {
let subtitlesToggleAction = UIAction(title: "Toggle Subtitles") { [weak self] _ in
guard let self = self else { return }
self.subtitlesEnabled.toggle()
guard let self else { return }
subtitlesEnabled.toggle()
}
let foregroundActions = [
@ -2062,32 +2065,32 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
let delayActions = [
UIAction(title: "-0.5s") { [weak self] _ in
guard let self = self else { return }
self.adjustSubtitleDelay(by: -0.5)
guard let self else { return }
adjustSubtitleDelay(by: -0.5)
},
UIAction(title: "-0.2s") { [weak self] _ in
guard let self = self else { return }
self.adjustSubtitleDelay(by: -0.2)
guard let self else { return }
adjustSubtitleDelay(by: -0.2)
},
UIAction(title: "+0.2s") { [weak self] _ in
guard let self = self else { return }
self.adjustSubtitleDelay(by: 0.2)
guard let self else { return }
adjustSubtitleDelay(by: 0.2)
},
UIAction(title: "+0.5s") { [weak self] _ in
guard let self = self else { return }
self.adjustSubtitleDelay(by: 0.5)
guard let self else { return }
adjustSubtitleDelay(by: 0.5)
},
UIAction(title: "Custom...") { [weak self] _ in
guard let self = self else { return }
self.presentCustomDelayAlert()
guard let self else { return }
presentCustomDelayAlert()
}
]
let resetDelayAction = UIAction(title: "Reset Delay") { [weak self] _ in
guard let self = self else { return }
guard let self else { return }
SubtitleSettingsManager.shared.update { settings in settings.subtitleDelay = 0.0 }
self.subtitleDelay = 0.0
self.loadSubtitleSettings()
subtitleDelay = 0.0
loadSubtitleSettings()
}
let delayMenu = UIMenu(title: "Subtitle Delay", children: delayActions + [resetDelayAction])
@ -2187,11 +2190,11 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
}
override var prefersHomeIndicatorAutoHidden: Bool {
return true
true
}
override var prefersStatusBarHidden: Bool {
return true
true
}
func setupAudioSession() {
@ -2201,7 +2204,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
try audioSession.setActive(true)
try audioSession.overrideOutputAudioPort(.speaker)
} catch {
Logger.shared.log("Didn't set up AVAudioSession: \(error)", type: "Debug")
Logger.shared.log("Didn't set up AVAudioSession: \(error)", type: .debug)
}
volumeObserver = audioSession.observe(\.outputVolume, options: [.new]) { [weak self] _, change in
@ -2211,7 +2214,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
}
DispatchQueue.main.async {
self?.volumeViewModel.value = Double(newVol)
Logger.shared.log("Hardware volume changed, new value: \(newVol)", type: "Debug")
Logger.shared.log("Hardware volume changed, new value: \(newVol)", type: .debug)
}
}
}
@ -2219,7 +2222,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
private func setupHoldGesture() {
holdGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleHoldGesture(_:)))
holdGesture?.minimumPressDuration = 0.5
if let holdGesture = holdGesture {
if let holdGesture {
view.addGestureRecognizer(holdGesture)
}
}
@ -2251,7 +2254,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
}
private func beginHoldSpeed() {
guard let player = player else { return }
guard let player else { return }
originalRate = player.rate
let holdSpeed = UserDefaults.standard.float(forKey: "holdSpeedPlayer")
let speed = holdSpeed > 0 ? holdSpeed : 2.0
@ -2282,7 +2285,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
guard self != nil else { return }
if player.timeControlStatus == .paused,
let reason = player.reasonForWaitingToPlay {
Logger.shared.log("Paused reason: \(reason)", type: "Error")
Logger.shared.log("Paused reason: \(reason)", type: .error)
if reason == .toMinimizeStalls {
player.play()
}

View file

@ -56,7 +56,7 @@ class NormalPlayer: AVPlayerViewController {
try audioSession.overrideOutputAudioPort(.speaker)
} catch {
Logger.shared.log("Didn't set up AVAudioSession: \(error)", type: "Debug")
Logger.shared.log("Didn't set up AVAudioSession: \(error)", type: .debug)
}
}
}

View file

@ -5,8 +5,8 @@
// Created by Francesco on 09/01/25.
//
import UIKit
import AVKit
import UIKit
class VideoPlayerViewController: UIViewController {
let module: ScrapingModule
@ -16,13 +16,13 @@ class VideoPlayerViewController: UIViewController {
var playerViewController: NormalPlayer?
var timeObserverToken: Any?
var streamUrl: String?
var fullUrl: String = ""
var subtitles: String = ""
var aniListID: Int = 0
var fullUrl = ""
var subtitles = ""
var aniListID = 0
var episodeNumber: Int = 0
var episodeImageUrl: String = ""
var mediaTitle: String = ""
var episodeNumber = 0
var episodeImageUrl = ""
var mediaTitle = ""
init(module: ScrapingModule, continueWatchingManager: ContinueWatchingManager) {
self.module = module
@ -30,6 +30,7 @@ class VideoPlayerViewController: UIViewController {
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@ -37,7 +38,7 @@ class VideoPlayerViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
guard let streamUrl = streamUrl, let url = URL(string: streamUrl) else {
guard let streamUrl, let url = URL(string: streamUrl) else {
return
}
@ -54,7 +55,7 @@ class VideoPlayerViewController: UIViewController {
playerViewController = NormalPlayer()
playerViewController?.player = player
if let playerViewController = playerViewController {
if let playerViewController {
addChild(playerViewController)
playerViewController.view.frame = view.bounds
playerViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
@ -86,7 +87,7 @@ class VideoPlayerViewController: UIViewController {
UserDefaults.standard.set(playbackSpeed, forKey: "lastPlaybackSpeed")
}
player?.pause()
if let timeObserverToken = timeObserverToken {
if let timeObserverToken {
player?.removeTimeObserver(timeObserverToken)
self.timeObserverToken = nil
}
@ -104,7 +105,7 @@ class VideoPlayerViewController: UIViewController {
let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
guard let self = self,
guard let self,
let currentItem = player.currentItem,
currentItem.duration.seconds.isFinite else {
return
@ -116,34 +117,34 @@ class VideoPlayerViewController: UIViewController {
UserDefaults.standard.set(currentTime, forKey: "lastPlayedTime_\(fullURL)")
UserDefaults.standard.set(duration, forKey: "totalTime_\(fullURL)")
if let streamUrl = self.streamUrl {
if let streamUrl {
let progress = min(max(currentTime / duration, 0), 1.0)
let item = ContinueWatchingItem(
id: UUID(),
imageUrl: self.episodeImageUrl,
episodeNumber: self.episodeNumber,
mediaTitle: self.mediaTitle,
imageUrl: episodeImageUrl,
episodeNumber: episodeNumber,
mediaTitle: mediaTitle,
progress: progress,
streamUrl: streamUrl,
fullUrl: self.fullUrl,
subtitles: self.subtitles,
aniListID: self.aniListID,
module: self.module
fullUrl: fullUrl,
subtitles: subtitles,
aniListID: aniListID,
module: module
)
continueWatchingManager.save(item: item)
}
let remainingPercentage = (duration - currentTime) / duration
if remainingPercentage < 0.1 && self.aniListID != 0 {
if remainingPercentage < 0.1 && aniListID != 0 {
let aniListMutation = AniListMutation()
aniListMutation.updateAnimeProgress(animeId: self.aniListID, episodeNumber: self.episodeNumber) { result in
aniListMutation.updateAnimeProgress(animeId: aniListID, episodeNumber: episodeNumber) { result in
switch result {
case .success:
Logger.shared.log("Successfully updated AniList progress for episode \(self.episodeNumber)", type: "General")
Logger.shared.log("Successfully updated AniList progress for episode \(self.episodeNumber)", type: .general)
case .failure(let error):
Logger.shared.log("Failed to update AniList progress: \(error.localizedDescription)", type: "Error")
Logger.shared.log("Failed to update AniList progress: \(error.localizedDescription)", type: .error)
}
}
}
@ -159,16 +160,16 @@ class VideoPlayerViewController: UIViewController {
}
override var prefersHomeIndicatorAutoHidden: Bool {
return true
true
}
override var prefersStatusBarHidden: Bool {
return true
true
}
deinit {
player?.pause()
if let timeObserverToken = timeObserverToken {
if let timeObserverToken {
player?.removeTimeObserver(timeObserverToken)
}
}

View file

@ -151,7 +151,7 @@ struct ModuleAdditionSettingsView: View {
await MainActor.run {
errorMessage = "Invalid URL"
isLoading = false
Logger.shared.log("Failed to open add module ui with url: \(moduleUrl)", type: "Error")
Logger.shared.log("Failed to open add module ui with url: \(moduleUrl)", type: .error)
}
return
}

View file

@ -18,9 +18,9 @@ class ModuleManager: ObservableObject {
if !FileManager.default.fileExists(atPath: url.path) {
do {
try "[]".write(to: url, atomically: true, encoding: .utf8)
Logger.shared.log("Created empty modules file", type: "Info")
Logger.shared.log("Created empty modules file", type: .info)
} catch {
Logger.shared.log("Failed to create modules file: \(error.localizedDescription)", type: "Error")
Logger.shared.log("Failed to create modules file: \(error.localizedDescription)", type: .error)
}
}
loadModules()
@ -38,7 +38,7 @@ class ModuleManager: ObservableObject {
let url = getModulesFilePath()
guard FileManager.default.fileExists(atPath: url.path) else {
Logger.shared.log("No modules file found after sync", type: "Error")
Logger.shared.log("No modules file found after sync", type: .error)
modules = []
return
}
@ -53,7 +53,7 @@ class ModuleManager: ObservableObject {
}
Logger.shared.log("Reloaded modules after iCloud sync")
} catch {
Logger.shared.log("Error handling modules sync: \(error.localizedDescription)", type: "Error")
Logger.shared.log("Error handling modules sync: \(error.localizedDescription)", type: .error)
modules = []
}
}
@ -71,12 +71,12 @@ class ModuleManager: ObservableObject {
let url = getModulesFilePath()
guard FileManager.default.fileExists(atPath: url.path) else {
Logger.shared.log("Modules file does not exist, creating empty one", type: "Info")
Logger.shared.log("Modules file does not exist, creating empty one", type: .info)
do {
try "[]".write(to: url, atomically: true, encoding: .utf8)
modules = []
} catch {
Logger.shared.log("Failed to create modules file: \(error.localizedDescription)", type: "Error")
Logger.shared.log("Failed to create modules file: \(error.localizedDescription)", type: .error)
modules = []
}
return
@ -92,18 +92,18 @@ class ModuleManager: ObservableObject {
await checkJSModuleFiles()
}
} catch {
Logger.shared.log("Failed to decode modules: \(error.localizedDescription)", type: "Error")
Logger.shared.log("Failed to decode modules: \(error.localizedDescription)", type: .error)
try "[]".write(to: url, atomically: true, encoding: .utf8)
modules = []
}
} catch {
Logger.shared.log("Failed to load modules file: \(error.localizedDescription)", type: "Error")
Logger.shared.log("Failed to load modules file: \(error.localizedDescription)", type: .error)
modules = []
}
}
func checkJSModuleFiles() async {
Logger.shared.log("Checking JS module files...", type: "Info")
Logger.shared.log("Checking JS module files...", type: .info)
var missingCount = 0
for module in modules {
@ -112,30 +112,30 @@ class ModuleManager: ObservableObject {
missingCount += 1
do {
guard let scriptUrl = URL(string: module.metadata.scriptUrl) else {
Logger.shared.log("Invalid script URL for module: \(module.metadata.sourceName)", type: "Error")
Logger.shared.log("Invalid script URL for module: \(module.metadata.sourceName)", type: .error)
continue
}
Logger.shared.log("Downloading missing JS file for: \(module.metadata.sourceName)", type: "Info")
Logger.shared.log("Downloading missing JS file for: \(module.metadata.sourceName)", type: .info)
let (scriptData, _) = try await URLSession.custom.data(from: scriptUrl)
guard let jsContent = String(data: scriptData, encoding: .utf8) else {
Logger.shared.log("Invalid script encoding for module: \(module.metadata.sourceName)", type: "Error")
Logger.shared.log("Invalid script encoding for module: \(module.metadata.sourceName)", type: .error)
continue
}
try jsContent.write(to: localUrl, atomically: true, encoding: .utf8)
Logger.shared.log("Successfully downloaded JS file for module: \(module.metadata.sourceName)")
} catch {
Logger.shared.log("Failed to download JS file for module: \(module.metadata.sourceName) - \(error.localizedDescription)", type: "Error")
Logger.shared.log("Failed to download JS file for module: \(module.metadata.sourceName) - \(error.localizedDescription)", type: .error)
}
}
}
if missingCount > 0 {
Logger.shared.log("Downloaded \(missingCount) missing module JS files", type: "Info")
Logger.shared.log("Downloaded \(missingCount) missing module JS files", type: .info)
} else {
Logger.shared.log("All module JS files are present", type: "Info")
Logger.shared.log("All module JS files are present", type: .info)
}
}

View file

@ -42,7 +42,7 @@ class ProfileStore: ObservableObject {
fatalError("This can only fail if suiteName == app bundle id ...")
}
Logger.shared.log("loaded UserDefaults suite for '\(currentProfile.name)'", type: "Profile")
Logger.shared.log("loaded UserDefaults suite for '\(currentProfile.name)'", type: .info)
return suite
}

View file

@ -168,14 +168,14 @@ class ICloudSyncManager {
try FileManager.default.copyItem(at: localModulesURL, to: iCloudModulesURL)
}
} catch {
Logger.shared.log("iCloud modules sync error: \(error)", type: "Error")
Logger.shared.log("iCloud modules sync error: \(error)", type: .error)
}
}
}
func syncModulesFromiCloud() {
guard let iCloudURL = self.ubiquityContainerURL else {
Logger.shared.log("iCloud container not available", type: "Error")
Logger.shared.log("iCloud container not available", type: .error)
return
}
@ -184,13 +184,13 @@ class ICloudSyncManager {
do {
if !FileManager.default.fileExists(atPath: iCloudModulesURL.path) {
Logger.shared.log("No modules file found in iCloud", type: "Info")
Logger.shared.log("No modules file found in iCloud", type: .info)
if FileManager.default.fileExists(atPath: localModulesURL.path) {
Logger.shared.log("Copying local modules file to iCloud", type: "Info")
Logger.shared.log("Copying local modules file to iCloud", type: .info)
try FileManager.default.copyItem(at: localModulesURL, to: iCloudModulesURL)
} else {
Logger.shared.log("Creating new empty modules file in iCloud", type: "Info")
Logger.shared.log("Creating new empty modules file in iCloud", type: .info)
let emptyModules: [ScrapingModule] = []
let emptyData = try JSONEncoder().encode(emptyModules)
try emptyData.write(to: iCloudModulesURL)
@ -214,7 +214,7 @@ class ICloudSyncManager {
}
if shouldCopy {
Logger.shared.log("Syncing modules from iCloud", type: "Info")
Logger.shared.log("Syncing modules from iCloud", type: .info)
if FileManager.default.fileExists(atPath: localModulesURL.path) {
try FileManager.default.removeItem(at: localModulesURL)
}
@ -225,7 +225,7 @@ class ICloudSyncManager {
}
}
} catch {
Logger.shared.log("iCloud modules sync error: \(error)", type: "Error")
Logger.shared.log("iCloud modules sync error: \(error)", type: .error)
}
}

View file

@ -199,7 +199,7 @@ struct ExploreView: View {
Menu {
if getModuleLanguageGroups().isEmpty {
Button("No modules available") {
print("[Error] No Modules Button clicked")
Logger.shared.log("No Modules Button clicked", type: .error)
}
.disabled(true)
@ -262,7 +262,7 @@ struct ExploreView: View {
}
private func fetchData() {
Logger.shared.log("Fetching Explore Data", type: "General")
Logger.shared.log("Fetching Explore Data", type: .general)
guard let module = selectedModule else {
exploreItems = []
hasNoResults = false
@ -292,7 +292,7 @@ struct ExploreView: View {
}
}
} catch {
Logger.shared.log("Error loading module: \(error)", type: "Error")
Logger.shared.log("Error loading module: \(error)", type: .error)
isLoading = false
hasNoResults = true
}

View file

@ -52,14 +52,14 @@ class LibraryManager: ObservableObject {
if let index = bookmarks.firstIndex(where: { $0.id == item.id }) {
bookmarks.remove(at: index)
Logger.shared.log("Removed series \(item.id) from bookmarks.", type: "Debug")
Logger.shared.log("Removed series \(item.id) from bookmarks.", type: .debug)
saveBookmarks()
}
}
private func loadBookmarks() {
guard let data = userDefaultsSuite.data(forKey: bookmarksKey) else {
Logger.shared.log("No bookmarks data found in UserDefaults.", type: "Debug")
Logger.shared.log("No bookmarks data found in UserDefaults.", type: .debug)
bookmarks = []
return
}
@ -67,7 +67,7 @@ class LibraryManager: ObservableObject {
do {
bookmarks = try JSONDecoder().decode([LibraryItem].self, from: data)
} catch {
Logger.shared.log("Failed to decode bookmarks: \(error.localizedDescription)", type: "Error")
Logger.shared.log("Failed to decode bookmarks: \(error.localizedDescription)", type: .error)
bookmarks = []
}
}
@ -77,7 +77,7 @@ class LibraryManager: ObservableObject {
let encoded = try JSONEncoder().encode(bookmarks)
userDefaultsSuite.set(encoded, forKey: bookmarksKey)
} catch {
Logger.shared.log("Failed to save bookmarks: \(error)", type: "Error")
Logger.shared.log("Failed to save bookmarks: \(error)", type: .error)
}
}

View file

@ -160,7 +160,7 @@ struct EpisodeCell: View {
URLSession.custom.dataTask(with: url) { data, _, error in
if let error {
Logger.shared.log("Failed to fetch anime episode details: \(error)", type: "Error")
Logger.shared.log("Failed to fetch anime episode details: \(error)", type: .error)
DispatchQueue.main.async { isLoading = false }
return
}
@ -177,7 +177,7 @@ struct EpisodeCell: View {
let episodeDetails = episodes["\(episodeID + 1)"] as? [String: Any],
let title = episodeDetails["title"] as? [String: String],
let image = episodeDetails["image"] as? String else {
Logger.shared.log("Invalid anime response format", type: "Error")
Logger.shared.log("Invalid anime response format", type: .error)
DispatchQueue.main.async { isLoading = false }
return
}

View file

@ -22,28 +22,28 @@ struct MediaInfoView: View {
let href: String
let module: ScrapingModule
@State var aliases: String = ""
@State var synopsis: String = ""
@State var airdate: String = ""
@State var episodeLinks: [EpisodeLink] = []
@State var itemID: Int?
@State var tmdbID: Int?
@State private var aliases = ""
@State private var synopsis = ""
@State private var airdate = ""
@State private var episodeLinks: [EpisodeLink] = []
@State private var itemID: Int?
@State private var tmdbID: Int?
@State var isLoading: Bool = true
@State var showFullSynopsis: Bool = false
@State var hasFetched: Bool = false
@State var isRefetching: Bool = true
@State var isFetchingEpisode: Bool = false
@State private var isLoading = true
@State private var showFullSynopsis = false
@State private var hasFetched = false
@State private var isRefetching = true
@State private var isFetchingEpisode = false
@State private var refreshTrigger: Bool = false
@State private var buttonRefreshTrigger: Bool = false
@State private var refreshTrigger = false
@State private var buttonRefreshTrigger = false
@State private var selectedEpisodeNumber: Int = 0
@State private var selectedEpisodeImage: String = ""
@State private var selectedSeason: Int = 0
@State private var selectedEpisodeNumber = 0
@State private var selectedEpisodeImage = ""
@State private var selectedSeason = 0
@AppStorage("externalPlayer") private var externalPlayer: String = "Default"
@AppStorage("episodeChunkSize") private var episodeChunkSize: Int = 100
@AppStorage("externalPlayer") private var externalPlayer = "Default"
@AppStorage("episodeChunkSize") private var episodeChunkSize = 100
@StateObject private var jsController = JSController()
@EnvironmentObject var moduleManager: ModuleManager
@ -53,16 +53,16 @@ struct MediaInfoView: View {
@State private var selectedRange: Range<Int> = 0..<100
@State private var showSettingsMenu = false
@State private var customAniListID: Int?
@State private var showStreamLoadingView: Bool = false
@State private var currentStreamTitle: String = ""
@State private var showStreamLoadingView = false
@State private var currentStreamTitle = ""
@State private var activeFetchID: UUID?
@Environment(\.dismiss) private var dismiss
@State private var orientationChanged: Bool = false
@State private var orientationChanged = false
private var isGroupedBySeasons: Bool {
return groupedEpisodes().count > 1
groupedEpisodes().count > 1
}
var body: some View {
@ -112,6 +112,7 @@ struct MediaInfoView: View {
.resizable()
.frame(width: 15, height: 15)
.foregroundColor(.secondary)
.accessibilityLabel("Calendar Icon")
Text(airdate)
.font(.system(size: 12))
@ -134,6 +135,7 @@ struct MediaInfoView: View {
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.primary)
.accessibilityLabel("Safari Icon")
}
.padding(4)
.background(Capsule().fill(Color.accentColor.opacity(0.4)))
@ -146,7 +148,7 @@ struct MediaInfoView: View {
Label("Set Custom AniList ID", systemImage: "number")
}
if let _ = customAniListID {
if customAniListID != nil {
Button(action: {
customAniListID = nil
itemID = nil
@ -176,7 +178,7 @@ struct MediaInfoView: View {
Divider()
Button(action: {
Logger.shared.log("Debug Info:\nTitle: \(title)\nHref: \(href)\nModule: \(module.metadata.sourceName)\nAniList ID: \(itemID ?? -1)\nCustom ID: \(customAniListID ?? -1)", type: "Debug")
Logger.shared.log("Debug Info:\nTitle: \(title)\nHref: \(href)\nModule: \(module.metadata.sourceName)\nAniList ID: \(itemID ?? -1)\nCustom ID: \(customAniListID ?? -1)", type: .debug)
DropManager.shared.showDrop(title: "Debug Info Logged", subtitle: "", duration: 1.0, icon: UIImage(systemName: "terminal"))
}) {
Label("Log Debug Info", systemImage: "terminal")
@ -186,6 +188,7 @@ struct MediaInfoView: View {
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.primary)
.accessibilityLabel("More Icon")
}
}
}
@ -221,6 +224,7 @@ struct MediaInfoView: View {
HStack {
Image(systemName: "play.fill")
.foregroundColor(.primary)
.accessibilityLabel("Play Icon")
Text(startWatchingText)
.font(.headline)
.foregroundColor(.primary)
@ -246,6 +250,7 @@ struct MediaInfoView: View {
.resizable()
.frame(width: 20, height: 27)
.foregroundColor(Color.accentColor)
.accessibilityLabel("Bookmark Icon")
}
}
@ -287,6 +292,7 @@ struct MediaInfoView: View {
}
}
}
if isGroupedBySeasons {
let seasons = groupedEpisodes()
if !seasons.isEmpty, selectedSeason < seasons.count {
@ -318,8 +324,8 @@ struct MediaInfoView: View {
for ep2 in seasons[selectedSeason] where ep2.number < ep.number {
let href = ep2.href
updates["lastPlayedTime_\(href)"] = 99999999.0
updates["totalTime_\(href)"] = 99999999.0
updates["lastPlayedTime_\(href)"] = 99_999_999.0
updates["totalTime_\(href)"] = 99_999_999.0
}
for (key, value) in updates {
@ -329,7 +335,7 @@ struct MediaInfoView: View {
userDefaults.synchronize()
refreshTrigger.toggle()
Logger.shared.log("Marked episodes watched within season \(selectedSeason + 1) of \"\(title)\".", type: "General")
Logger.shared.log("Marked episodes watched within season \(selectedSeason + 1) of \"\(title)\".", type: .general)
}
)
.id(refreshTrigger)
@ -379,7 +385,7 @@ struct MediaInfoView: View {
}
refreshTrigger.toggle()
Logger.shared.log("Marked \(ep.number - 1) episodes watched within series \"\(title)\".", type: "General")
Logger.shared.log("Marked \(ep.number - 1) episodes watched within series \"\(title)\".", type: .general)
}
)
.id(refreshTrigger)
@ -401,6 +407,7 @@ struct MediaInfoView: View {
Image(systemName: "exclamationmark.triangle")
.font(.largeTitle)
.foregroundColor(.secondary)
.accessibilityLabel("Warning Icon")
HStack(spacing: 2) {
Text("No episodes Found:")
.foregroundColor(.secondary)
@ -425,37 +432,6 @@ struct MediaInfoView: View {
}
}
}
.onAppear {
buttonRefreshTrigger.toggle()
if !hasFetched {
DropManager.shared.showDrop(title: "Fetching Data", subtitle: "Please wait while fetching.", duration: 0.5, icon: UIImage(systemName: "arrow.triangle.2.circlepath"))
fetchDetails()
if let savedID = UserDefaults.standard.object(forKey: "custom_anilist_id_\(href)") as? Int {
customAniListID = savedID
itemID = savedID
Logger.shared.log("Using custom AniList ID: \(savedID)", type: "Debug")
} else {
fetchItemID(byTitle: cleanTitle(title)) { result in
switch result {
case .success(let id):
itemID = id
case .failure(let error):
Logger.shared.log("Failed to fetch AniList ID: \(error)")
AnalyticsManager.shared.sendEvent(event: "error", additionalData: ["error": error, "message": "Failed to fetch AniList ID"])
}
}
}
hasFetched = true
AnalyticsManager.shared.sendEvent(event: "search", additionalData: ["title": title])
}
selectedRange = 0..<episodeChunkSize
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
orientationChanged.toggle()
}
if showStreamLoadingView {
VStack(spacing: 16) {
@ -474,7 +450,7 @@ struct MediaInfoView: View {
.padding(.vertical, 8)
.padding(.horizontal, 24)
.background(
Color(red: 1.0, green: 112/255.0, blue: 94/255.0)
Color(red: 1.0, green: 112 / 255.0, blue: 94 / 255.0)
)
.foregroundColor(.white)
.cornerRadius(8)
@ -501,11 +477,43 @@ struct MediaInfoView: View {
}) {
HStack {
Image(systemName: "chevron.left")
.accessibilityLabel("Right Arrow Icon")
Text("Search")
}
}
}
}
.onAppear {
buttonRefreshTrigger.toggle()
if !hasFetched {
DropManager.shared.showDrop(title: "Fetching Data", subtitle: "Please wait while fetching.", duration: 0.5, icon: UIImage(systemName: "arrow.triangle.2.circlepath"))
fetchDetails()
if let savedID = UserDefaults.standard.object(forKey: "custom_anilist_id_\(href)") as? Int {
customAniListID = savedID
itemID = savedID
Logger.shared.log("Using custom AniList ID: \(savedID)", type: .debug)
} else {
fetchItemID(byTitle: cleanTitle(title)) { result in
switch result {
case .success(let id):
itemID = id
case .failure(let error):
Logger.shared.log("Failed to fetch AniList ID: \(error)", type: .general)
AnalyticsManager.shared.sendEvent(event: "error", additionalData: ["error": error, "message": "Failed to fetch AniList ID"])
}
}
}
hasFetched = true
AnalyticsManager.shared.sendEvent(event: "search", additionalData: ["title": title])
}
selectedRange = 0..<episodeChunkSize
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
orientationChanged.toggle()
}
.onDisappear {
activeFetchID = nil
isFetchingEpisode = false
@ -630,7 +638,7 @@ struct MediaInfoView: View {
}
}
} catch {
Logger.shared.log("Error loading module: \(error)", type: "Error")
Logger.shared.log("Error loading module: \(error)", type: .error)
isLoading = false
isRefetching = false
}
@ -653,114 +661,114 @@ struct MediaInfoView: View {
if module.metadata.softsub == true {
if module.metadata.asyncJS == true {
jsController.fetchStreamUrlJS(episodeUrl: href, softsub: true, module: module) { result in
guard self.activeFetchID == fetchID else { return }
guard activeFetchID == fetchID else { return }
if let streams = result.streams, !streams.isEmpty {
if streams.count > 1 {
self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first)
showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first)
} else {
self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first)
playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first)
}
} else {
self.handleStreamFailure(error: nil)
handleStreamFailure(error: nil)
}
DispatchQueue.main.async {
self.isFetchingEpisode = false
isFetchingEpisode = false
}
}
} else if module.metadata.streamAsyncJS == true {
jsController.fetchStreamUrlJSSecond(episodeUrl: href, softsub: true, module: module) { result in
guard self.activeFetchID == fetchID else { return }
guard activeFetchID == fetchID else { return }
if let streams = result.streams, !streams.isEmpty {
if streams.count > 1 {
self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first)
showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first)
} else {
self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first)
playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first)
}
} else {
self.handleStreamFailure(error: nil)
handleStreamFailure(error: nil)
}
DispatchQueue.main.async {
self.isFetchingEpisode = false
isFetchingEpisode = false
}
}
} else {
jsController.fetchStreamUrl(episodeUrl: href, softsub: true, module: module) { result in
guard self.activeFetchID == fetchID else { return }
guard activeFetchID == fetchID else { return }
if let streams = result.streams, !streams.isEmpty {
if streams.count > 1 {
self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first)
showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first)
} else {
self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first)
playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first)
}
} else {
self.handleStreamFailure(error: nil)
handleStreamFailure(error: nil)
}
DispatchQueue.main.async {
self.isFetchingEpisode = false
isFetchingEpisode = false
}
}
}
} else {
if module.metadata.asyncJS == true {
jsController.fetchStreamUrlJS(episodeUrl: href, module: module) { result in
guard self.activeFetchID == fetchID else { return }
guard activeFetchID == fetchID else { return }
if let streams = result.streams, !streams.isEmpty {
if streams.count > 1 {
self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first)
showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first)
} else {
self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first)
playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first)
}
} else {
self.handleStreamFailure(error: nil)
handleStreamFailure(error: nil)
}
DispatchQueue.main.async {
self.isFetchingEpisode = false
isFetchingEpisode = false
}
}
} else if module.metadata.streamAsyncJS == true {
jsController.fetchStreamUrlJSSecond(episodeUrl: href, module: module) { result in
guard self.activeFetchID == fetchID else { return }
guard activeFetchID == fetchID else { return }
if let streams = result.streams, !streams.isEmpty {
if streams.count > 1 {
self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first)
showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first)
} else {
self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first)
playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first)
}
} else {
self.handleStreamFailure(error: nil)
handleStreamFailure(error: nil)
}
DispatchQueue.main.async {
self.isFetchingEpisode = false
isFetchingEpisode = false
}
}
} else {
jsController.fetchStreamUrl(episodeUrl: href, module: module) { result in
guard self.activeFetchID == fetchID else { return }
guard activeFetchID == fetchID else { return }
if let streams = result.streams, !streams.isEmpty {
if streams.count > 1 {
self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first)
showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first)
} else {
self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first)
playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first)
}
} else {
self.handleStreamFailure(error: nil)
handleStreamFailure(error: nil)
}
DispatchQueue.main.async {
self.isFetchingEpisode = false
isFetchingEpisode = false
}
}
}
}
} catch {
self.handleStreamFailure(error: error)
handleStreamFailure(error: error)
DispatchQueue.main.async {
self.isFetchingEpisode = false
isFetchingEpisode = false
}
}
}
@ -771,7 +779,7 @@ struct MediaInfoView: View {
self.isFetchingEpisode = false
self.showStreamLoadingView = false
if let error {
Logger.shared.log("Error loading module: \(error)", type: "Error")
Logger.shared.log("Error loading module: \(error)", type: .error)
AnalyticsManager.shared.sendEvent(event: "error", additionalData: ["error": error, "message": "Failed to fetch stream"])
}
DropManager.shared.showDrop(title: "Stream not Found", subtitle: "", duration: 0.5, icon: UIImage(systemName: "xmark"))
@ -810,7 +818,7 @@ struct MediaInfoView: View {
}
alert.addAction(UIAlertAction(title: title, style: .default) { _ in
self.playStream(url: streamUrl, fullURL: fullURL, subtitles: subtitles)
playStream(url: streamUrl, fullURL: fullURL, subtitles: subtitles)
})
streamIndex += 1
@ -821,7 +829,6 @@ struct MediaInfoView: View {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first,
let rootVC = window.rootViewController {
if UIDevice.current.userInterfaceIdiom == .pad {
if let popover = alert.popoverPresentationController {
popover.sourceView = window
@ -839,7 +846,7 @@ struct MediaInfoView: View {
}
DispatchQueue.main.async {
self.isFetchingEpisode = false
isFetchingEpisode = false
}
}
}
@ -885,12 +892,12 @@ struct MediaInfoView: View {
break
}
if let scheme = scheme, let url = URL(string: scheme), UIApplication.shared.canOpenURL(url) {
if let scheme, let url = URL(string: scheme), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
Logger.shared.log("Opening external app with scheme: \(url)", type: "General")
Logger.shared.log("Opening external app with scheme: \(url)", type: .general)
} else {
guard let url = URL(string: url) else {
Logger.shared.log("Invalid stream URL: \(url)", type: "Error")
Logger.shared.log("Invalid stream URL: \(url)", type: .error)
DropManager.shared.showDrop(title: "Error", subtitle: "Invalid stream URL", duration: 2.0, icon: UIImage(systemName: "xmark.circle"))
return
}
@ -916,7 +923,7 @@ struct MediaInfoView: View {
let rootVC = windowScene.windows.first?.rootViewController {
FindTopViewController.findViewController(rootVC).present(customMediaPlayer, animated: true, completion: nil)
} else {
Logger.shared.log("Failed to find root view controller", type: "Error")
Logger.shared.log("Failed to find root view controller", type: .error)
DropManager.shared.showDrop(title: "Error", subtitle: "Failed to present player", duration: 2.0, icon: UIImage(systemName: "xmark.circle"))
}
}
@ -926,7 +933,7 @@ struct MediaInfoView: View {
private func selectNextEpisode() {
guard let currentIndex = episodeLinks.firstIndex(where: { $0.number == selectedEpisodeNumber }),
currentIndex + 1 < episodeLinks.count else {
Logger.shared.log("No more episodes to play", type: "Info")
Logger.shared.log("No more episodes to play", type: .info)
return
}
@ -938,7 +945,7 @@ struct MediaInfoView: View {
private func openSafariViewController(with urlString: String) {
guard let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) else {
Logger.shared.log("Unable to open the webpage", type: "Error")
Logger.shared.log("Unable to open the webpage", type: .error)
return
}
let safariViewController = SFSafariViewController(url: url)
@ -949,7 +956,7 @@ struct MediaInfoView: View {
}
private func cleanTitle(_ title: String?) -> String {
guard let title = title else { return "Unknown" }
guard let title else { return "Unknown" }
let cleaned = title.replacingOccurrences(
of: "\\s*\\([^\\)]*\\)",
@ -1026,7 +1033,7 @@ struct MediaInfoView: View {
customAniListID = id
itemID = id
UserDefaults.standard.set(id, forKey: "custom_anilist_id_\(href)")
Logger.shared.log("Set custom AniList ID: \(id)", type: "General")
Logger.shared.log("Set custom AniList ID: \(id)", type: .general)
}
})

View file

@ -218,7 +218,7 @@ struct SearchView: View {
Menu {
if getModuleLanguageGroups().isEmpty {
Button("No modules available") {
print("[Error] No Modules Button clicked")
Logger.shared.log("No Modules Button clicked", type: .error)
}
.disabled(true)
@ -290,7 +290,7 @@ struct SearchView: View {
}
private func performSearch() {
Logger.shared.log("Searching for: \(searchText)", type: "General")
Logger.shared.log("Searching for: \(searchText)", type: .general)
guard !searchText.isEmpty, let module = selectedModule else {
searchItems = []
hasNoResults = false
@ -320,7 +320,7 @@ struct SearchView: View {
}
}
} catch {
Logger.shared.log("Error loading module: \(error)", type: "Error")
Logger.shared.log("Error loading module: \(error)", type: .error)
isSearching = false
hasNoResults = true
}

View file

@ -63,7 +63,7 @@ struct SettingsViewAlternateAppIconPicker: View {
UIApplication.shared.setAlternateIconName(iconName == "Default" ? nil : "AppIcon_\(iconName)", completionHandler: { error in
isPresented = false
if let error {
print("Failed to set alternate icon: \(error.localizedDescription)")
Logger.shared.log("Failed to set alternate icon: \(error.localizedDescription)", type: .error)
}
})
}

View file

@ -61,7 +61,7 @@ struct SettingsViewData: View {
if let domain = Bundle.main.bundleIdentifier {
UserDefaults.standard.removePersistentDomain(forName: domain)
UserDefaults.standard.synchronize()
Logger.shared.log("Cleared app data!", type: "General")
Logger.shared.log("Cleared app data!", type: .general)
exit(0)
}
}
@ -75,10 +75,10 @@ struct SettingsViewData: View {
for filePath in filePaths {
try FileManager.default.removeItem(at: filePath)
}
Logger.shared.log("Cache cleared successfully!", type: "General")
Logger.shared.log("Cache cleared successfully!", type: .general)
}
} catch {
Logger.shared.log("Failed to clear cache.", type: "Error")
Logger.shared.log("Failed to clear cache.", type: .error)
}
}
@ -90,10 +90,10 @@ struct SettingsViewData: View {
for fileURL in fileURLs {
try fileManager.removeItem(at: fileURL)
}
Logger.shared.log("All files in documents folder removed", type: "General")
Logger.shared.log("All files in documents folder removed", type: .general)
exit(0)
} catch {
Logger.shared.log("Error removing files in documents folder: \(error)", type: "Error")
Logger.shared.log("Error removing files in documents folder: \(error)", type: .error)
}
}
}

View file

@ -121,12 +121,12 @@ struct SettingsViewGeneral: View {
HStack {
if UIDevice.current.userInterfaceIdiom == .pad {
Picker("Portrait Columns", selection: $mediaColumnsPortrait) {
ForEach(1..<6) { i in Text("\(i)").tag(i) }
ForEach(1 ..< 6) { i in Text("\(i)").tag(i) }
}
.pickerStyle(MenuPickerStyle())
} else {
Picker("Portrait Columns", selection: $mediaColumnsPortrait) {
ForEach(1..<5) { i in Text("\(i)").tag(i) }
ForEach(1 ..< 5) { i in Text("\(i)").tag(i) }
}
.pickerStyle(MenuPickerStyle())
}
@ -134,12 +134,12 @@ struct SettingsViewGeneral: View {
HStack {
if UIDevice.current.userInterfaceIdiom == .pad {
Picker("Landscape Columns", selection: $mediaColumnsLandscape) {
ForEach(2..<9) { i in Text("\(i)").tag(i) }
ForEach(2 ..< 9) { i in Text("\(i)").tag(i) }
}
.pickerStyle(MenuPickerStyle())
} else {
Picker("Landscape Columns", selection: $mediaColumnsLandscape) {
ForEach(2..<6) { i in Text("\(i)").tag(i) }
ForEach(2 ..< 6) { i in Text("\(i)").tag(i) }
}
.pickerStyle(MenuPickerStyle())
}

View file

@ -309,7 +309,7 @@ struct SettingsViewTrackers: View {
request.httpBody = try JSONSerialization.data(withJSONObject: body, options: [])
} catch {
anilistStatus = "Failed to serialize request"
Logger.shared.log("Failed to serialize request", type: "Error")
Logger.shared.log("Failed to serialize request", type: .error)
isAnilistLoading = false
return
}
@ -319,12 +319,12 @@ struct SettingsViewTrackers: View {
isAnilistLoading = false
if let error {
anilistStatus = "Error: \(error.localizedDescription)"
Logger.shared.log("Error: \(error.localizedDescription)", type: "Error")
Logger.shared.log("Error: \(error.localizedDescription)", type: .error)
return
}
guard let data else {
anilistStatus = "No data received"
Logger.shared.log("No data received", type: "Error")
Logger.shared.log("No data received", type: .error)
return
}
do {
@ -339,11 +339,11 @@ struct SettingsViewTrackers: View {
anilistStatus = "Logged in as \(name)"
} else {
anilistStatus = "Unexpected response format!"
Logger.shared.log("Unexpected response format!", type: "Error")
Logger.shared.log("Unexpected response format!", type: .error)
}
} catch {
anilistStatus = "Failed to parse response: \(error.localizedDescription)"
Logger.shared.log("Failed to parse response: \(error.localizedDescription)", type: "Error")
Logger.shared.log("Failed to parse response: \(error.localizedDescription)", type: .error)
}
}
}.resume()