diff --git a/Sora/SoraApp.swift b/Sora/SoraApp.swift index 79f8db0..d60ca21 100644 --- a/Sora/SoraApp.swift +++ b/Sora/SoraApp.swift @@ -89,11 +89,7 @@ struct SoraApp: App { } } .onOpenURL { url in - if let params = url.queryParameters, params["code"] != nil { - Self.handleRedirect(url: url) - } else { - handleURL(url) - } + handleURL(url) } } } @@ -142,38 +138,8 @@ struct SoraApp: App { break } } - - static func handleRedirect(url: URL) { - guard let params = url.queryParameters, - let code = params["code"] else { - Logger.shared.log("Failed to extract authorization code") - return - } - - switch url.host { - case "anilist": - AniListToken.exchangeAuthorizationCodeForToken(code: code) { success in - if success { - Logger.shared.log("AniList token exchange successful") - } else { - Logger.shared.log("AniList token exchange failed", type: "Error") - } - } - - case "trakt": - TraktToken.exchangeAuthorizationCodeForToken(code: code) { success in - if success { - Logger.shared.log("Trakt token exchange successful") - } else { - Logger.shared.log("Trakt token exchange failed", type: "Error") - } - } - - default: - Logger.shared.log("Unknown authentication service", type: "Error") - } - } } + class AppInfo: NSObject { @objc func getBundleIdentifier() -> String { return Bundle.main.bundleIdentifier ?? "me.cranci.sulfur" diff --git a/Sora/Tracking Services/AniList/Auth/Anilist-Login.swift b/Sora/Tracking Services/AniList/Auth/Anilist-Login.swift index de958b1..aa4aadf 100644 --- a/Sora/Tracking Services/AniList/Auth/Anilist-Login.swift +++ b/Sora/Tracking Services/AniList/Auth/Anilist-Login.swift @@ -16,19 +16,28 @@ class AniListLogin { static func authenticate() { let urlString = "\(authorizationEndpoint)?client_id=\(clientID)&redirect_uri=\(redirectURI)&response_type=code" guard let url = URL(string: urlString) else { + Logger.shared.log("Invalid authorization URL", type: "Error") return } - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [:]) { success in - if success { - Logger.shared.log("Safari opened successfully", type: "Debug") + WebAuthenticationManager.shared.authenticate(url: url, callbackScheme: "sora") { result in + switch result { + case .success(let callbackURL): + if let params = callbackURL.queryParameters, + let code = params["code"] { + AniListToken.exchangeAuthorizationCodeForToken(code: code) { success in + if success { + Logger.shared.log("AniList token exchange successful", type: "Debug") + } else { + Logger.shared.log("AniList token exchange failed", type: "Error") + } + } } else { - Logger.shared.log("Failed to open Safari", type: "Error") + Logger.shared.log("No authorization code in callback URL", type: "Error") } + case .failure(let error): + Logger.shared.log("Authentication failed: \(error.localizedDescription)", type: "Error") } - } else { - Logger.shared.log("Cannot open URL", type: "Error") } } } diff --git a/Sora/Tracking Services/Trakt/Auth/Trakt-Login.swift b/Sora/Tracking Services/Trakt/Auth/Trakt-Login.swift index fc0c9c3..3dd98d1 100644 --- a/Sora/Tracking Services/Trakt/Auth/Trakt-Login.swift +++ b/Sora/Tracking Services/Trakt/Auth/Trakt-Login.swift @@ -16,19 +16,28 @@ class TraktLogin { static func authenticate() { let urlString = "\(authorizationEndpoint)?client_id=\(clientID)&redirect_uri=\(redirectURI)&response_type=code" guard let url = URL(string: urlString) else { + Logger.shared.log("Invalid authorization URL", type: "Error") return } - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [:]) { success in - if success { - Logger.shared.log("Safari opened successfully", type: "Debug") + WebAuthenticationManager.shared.authenticate(url: url, callbackScheme: "sora") { result in + switch result { + case .success(let callbackURL): + if let params = callbackURL.queryParameters, + let code = params["code"] { + TraktToken.exchangeAuthorizationCodeForToken(code: code) { success in + if success { + Logger.shared.log("Trakt token exchange successful", type: "Debug") + } else { + Logger.shared.log("Trakt token exchange failed", type: "Error") + } + } } else { - Logger.shared.log("Failed to open Safari", type: "Error") + Logger.shared.log("No authorization code in callback URL", type: "Error") } + case .failure(let error): + Logger.shared.log("Authentication failed: \(error.localizedDescription)", type: "Error") } - } else { - Logger.shared.log("Cannot open URL", type: "Error") } } } diff --git a/Sora/Tracking Services/Trakt/Mutations/TraktPushUpdates.swift b/Sora/Tracking Services/Trakt/Mutations/TraktPushUpdates.swift index 1d5dd3b..53ba1a8 100644 --- a/Sora/Tracking Services/Trakt/Mutations/TraktPushUpdates.swift +++ b/Sora/Tracking Services/Trakt/Mutations/TraktPushUpdates.swift @@ -30,18 +30,7 @@ class TraktMutation { return token } - enum ExternalIDType { - case tmdb(Int) - - var dictionary: [String: Any] { - switch self { - case .tmdb(let id): - return ["tmdb": id] - } - } - } - - func markAsWatched(type: String, externalID: ExternalIDType, episodeNumber: Int? = nil, seasonNumber: Int? = nil, completion: @escaping (Result) -> Void) { + func markAsWatched(type: String, tmdbID: Int, episodeNumber: Int? = nil, seasonNumber: Int? = nil, completion: @escaping (Result) -> Void) { if let sendTraktUpdates = UserDefaults.standard.object(forKey: "sendTraktUpdates") as? Bool, sendTraktUpdates == false { return @@ -49,10 +38,12 @@ class TraktMutation { guard let userToken = getTokenFromKeychain() else { completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Access token not found"]))) + Logger.shared.log("Trakt Access token not found", type: "Error") return } let endpoint = "/sync/history" + let watchedAt = ISO8601DateFormatter().string(from: Date()) let body: [String: Any] switch type { @@ -60,7 +51,8 @@ class TraktMutation { body = [ "movies": [ [ - "ids": externalID.dictionary + "ids": ["tmdb": tmdbID], + "watched_at": watchedAt ] ] ] @@ -74,12 +66,15 @@ class TraktMutation { body = [ "shows": [ [ - "ids": externalID.dictionary, + "ids": ["tmdb": tmdbID], "seasons": [ [ "number": season, "episodes": [ - ["number": episode] + [ + "number": episode, + "watched_at": watchedAt + ] ] ] ] @@ -94,13 +89,13 @@ class TraktMutation { var request = URLRequest(url: apiURL.appendingPathComponent(endpoint)) request.httpMethod = "POST" - request.setValue("Bearer \(userToken)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("Bearer \(userToken)", forHTTPHeaderField: "Authorization") request.setValue("2", forHTTPHeaderField: "trakt-api-version") request.setValue(TraktToken.clientID, forHTTPHeaderField: "trakt-api-key") do { - request.httpBody = try JSONSerialization.data(withJSONObject: body) + request.httpBody = try JSONSerialization.data(withJSONObject: body, options: [.prettyPrinted]) } catch { completion(.failure(error)) return @@ -112,15 +107,26 @@ class TraktMutation { return } - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1 - completion(.failure(NSError(domain: "", code: statusCode, userInfo: [NSLocalizedDescriptionKey: "Unexpected response or status code"]))) - return - } + guard let httpResponse = response as? HTTPURLResponse else { + completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "No HTTP response"]))) + return + } - Logger.shared.log("Successfully updated watch status on Trakt", type: "Debug") - completion(.success(())) + if (200...299).contains(httpResponse.statusCode) { + if let data = data, let responseString = String(data: data, encoding: .utf8) { + Logger.shared.log("Trakt API Response: \(responseString)", type: "Debug") + } + Logger.shared.log("Successfully updated watch status on Trakt", type: "Debug") + completion(.success(())) + } else { + var errorMessage = "Unexpected status code: \(httpResponse.statusCode)" + if let data = data, + let errorJson = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let error = errorJson["error"] as? String { + errorMessage = error + } + completion(.failure(NSError(domain: "", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: errorMessage]))) + } } task.resume() diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index c0d7e0e..4cc6d23 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -1656,7 +1656,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele let traktMutation = TraktMutation() if self.isMovie { - traktMutation.markAsWatched(type: "movie", externalID: .tmdb(tmdbId)) { result in + traktMutation.markAsWatched(type: "movie", tmdbID: tmdbId) { result in switch result { case .success: Logger.shared.log("Successfully updated Trakt progress for movie", type: "General") @@ -1667,7 +1667,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele } else { traktMutation.markAsWatched( type: "episode", - externalID: .tmdb(tmdbId), + tmdbID: tmdbId, episodeNumber: self.episodeNumber, seasonNumber: self.seasonNumber ) { result in diff --git a/Sora/Utils/MediaPlayer/VideoPlayer.swift b/Sora/Utils/MediaPlayer/VideoPlayer.swift index c0a2bd5..4651392 100644 --- a/Sora/Utils/MediaPlayer/VideoPlayer.swift +++ b/Sora/Utils/MediaPlayer/VideoPlayer.swift @@ -219,7 +219,7 @@ class VideoPlayerViewController: UIViewController { let traktMutation = TraktMutation() if self.isMovie { - traktMutation.markAsWatched(type: "movie", externalID: .tmdb(tmdbId)) { result in + traktMutation.markAsWatched(type: "movie", tmdbID: tmdbId) { result in switch result { case .success: Logger.shared.log("Successfully updated Trakt progress for movie", type: "General") @@ -230,7 +230,7 @@ class VideoPlayerViewController: UIViewController { } else { traktMutation.markAsWatched( type: "episode", - externalID: .tmdb(tmdbId), + tmdbID: tmdbId, episodeNumber: self.episodeNumber, seasonNumber: self.seasonNumber ) { result in diff --git a/Sora/Utils/WebAuthentication/WebAuthenticationManager.swift b/Sora/Utils/WebAuthentication/WebAuthenticationManager.swift new file mode 100644 index 0000000..ba1b7f9 --- /dev/null +++ b/Sora/Utils/WebAuthentication/WebAuthenticationManager.swift @@ -0,0 +1,45 @@ +// +// WebAuthenticationManager.swift +// Sulfur +// +// Created by Francesco on 11/06/25. +// + +import AuthenticationServices + +class WebAuthenticationManager { + static let shared = WebAuthenticationManager() + private var webAuthSession: ASWebAuthenticationSession? + + func authenticate(url: URL, callbackScheme: String, completion: @escaping (Result) -> Void) { + webAuthSession = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackScheme) { callbackURL, error in + if let error = error { + completion(.failure(error)) + return + } + + if let callbackURL = callbackURL { + completion(.success(callbackURL)) + } else { + completion(.failure(NSError(domain: "WebAuthenticationManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "No callback URL received"]))) + } + } + + webAuthSession?.presentationContextProvider = WebAuthenticationPresentationContext.shared + webAuthSession?.prefersEphemeralWebBrowserSession = true + webAuthSession?.start() + } +} + +class WebAuthenticationPresentationContext: NSObject, ASWebAuthenticationPresentationContextProviding { + static let shared = WebAuthenticationPresentationContext() + + func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first else { + fatalError("No window found") + } + + return window + } +} diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index be242eb..d212f16 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 04F08EDF2DE10C1D006B29D9 /* TabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F08EDE2DE10C1A006B29D9 /* TabBar.swift */; }; 04F08EE22DE10C40006B29D9 /* TabItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F08EE12DE10C27006B29D9 /* TabItem.swift */; }; 04F08EE42DE10D6F006B29D9 /* AllBookmarks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F08EE32DE10D6B006B29D9 /* AllBookmarks.swift */; }; + 130326B62DF979AC00AEF610 /* WebAuthenticationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130326B52DF979AC00AEF610 /* WebAuthenticationManager.swift */; }; 130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */; }; 13103E8B2D58E028000F0673 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E8A2D58E028000F0673 /* View.swift */; }; 13103E8E2D58E04A000F0673 /* SkeletonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E8D2D58E04A000F0673 /* SkeletonCell.swift */; }; @@ -114,6 +115,7 @@ 04F08EDE2DE10C1A006B29D9 /* TabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBar.swift; sourceTree = ""; }; 04F08EE12DE10C27006B29D9 /* TabItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabItem.swift; sourceTree = ""; }; 04F08EE32DE10D6B006B29D9 /* AllBookmarks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllBookmarks.swift; sourceTree = ""; }; + 130326B52DF979AC00AEF610 /* WebAuthenticationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebAuthenticationManager.swift; sourceTree = ""; }; 130C6BF82D53A4C200DC1432 /* Sora.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Sora.entitlements; sourceTree = ""; }; 130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewData.swift; sourceTree = ""; }; 13103E8A2D58E028000F0673 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; @@ -260,6 +262,15 @@ path = Models; sourceTree = ""; }; + 130326B42DF979A300AEF610 /* WebAuthentication */ = { + isa = PBXGroup; + children = ( + 130326B52DF979AC00AEF610 /* WebAuthenticationManager.swift */, + ); + name = WebAuthentication; + path = Sora/Utils/WebAuthentication; + sourceTree = SOURCE_ROOT; + }; 13103E802D589D6C000F0673 /* Tracking Services */ = { isa = PBXGroup; children = ( @@ -378,6 +389,7 @@ 133D7C852D2BE2640075467E /* Utils */ = { isa = PBXGroup; children = ( + 130326B42DF979A300AEF610 /* WebAuthentication */, 0457C5962DE7712A000AFBD9 /* ViewModifiers */, 04F08EE02DE10C22006B29D9 /* Models */, 04F08EDD2DE10C05006B29D9 /* TabBar */, @@ -699,6 +711,7 @@ 1E26E9E72DA9577900B9DC02 /* VolumeSlider.swift in Sources */, 136BBE802DB1038000906B5E /* Notification+Name.swift in Sources */, 13DB46902D900A38008CBC03 /* URL.swift in Sources */, + 130326B62DF979AC00AEF610 /* WebAuthenticationManager.swift in Sources */, 04F08EE42DE10D6F006B29D9 /* AllBookmarks.swift in Sources */, 138FE1D02DECA00D00936D81 /* TMDB-FetchID.swift in Sources */, 130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */,