diff --git a/Sora/Tracking Services/Trakt/Mutations/TraktPushUpdates.swift b/Sora/Tracking Services/Trakt/Mutations/TraktPushUpdates.swift index 53ba1a8..4c2e0fa 100644 --- a/Sora/Tracking Services/Trakt/Mutations/TraktPushUpdates.swift +++ b/Sora/Tracking Services/Trakt/Mutations/TraktPushUpdates.swift @@ -25,29 +25,36 @@ class TraktMutation { guard status == errSecSuccess, let tokenData = item as? Data, let token = String(data: tokenData, encoding: .utf8) else { - return nil - } + return nil + } return token } 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 { + let sendTraktUpdates = UserDefaults.standard.object(forKey: "sendTraktUpdates") as? Bool ?? true + if !sendTraktUpdates { + Logger.shared.log("Trakt updates disabled by user preference", type: "Debug") + completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Trakt updates disabled by user"]))) return } + Logger.shared.log("Attempting to mark \(type) as watched - TMDB ID: \(tmdbID), Episode: \(episodeNumber ?? 0), Season: \(seasonNumber ?? 0)", type: "Debug") + guard let userToken = getTokenFromKeychain() else { + Logger.shared.log("Trakt access token not found in keychain", type: "Error") completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Access token not found"]))) - Logger.shared.log("Trakt Access token not found", type: "Error") return } + Logger.shared.log("Found Trakt access token, proceeding with API call", type: "Debug") + let endpoint = "/sync/history" let watchedAt = ISO8601DateFormatter().string(from: Date()) let body: [String: Any] switch type { case "movie": + Logger.shared.log("Preparing movie watch request for TMDB ID: \(tmdbID)", type: "Debug") body = [ "movies": [ [ @@ -59,10 +66,13 @@ class TraktMutation { case "episode": guard let episode = episodeNumber, let season = seasonNumber else { - completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Missing episode or season number"]))) + let errorMsg = "Missing episode (\(episodeNumber ?? -1)) or season (\(seasonNumber ?? -1)) number" + Logger.shared.log(errorMsg, type: "Error") + completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: errorMsg]))) return } + Logger.shared.log("Preparing episode watch request - TMDB ID: \(tmdbID), Season: \(season), Episode: \(episode)", type: "Debug") body = [ "shows": [ [ @@ -83,6 +93,7 @@ class TraktMutation { ] default: + Logger.shared.log("Invalid content type: \(type)", type: "Error") completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid content type"]))) return } @@ -95,36 +106,54 @@ class TraktMutation { request.setValue(TraktToken.clientID, forHTTPHeaderField: "trakt-api-key") do { - request.httpBody = try JSONSerialization.data(withJSONObject: body, options: [.prettyPrinted]) + let jsonData = try JSONSerialization.data(withJSONObject: body, options: [.prettyPrinted]) + request.httpBody = jsonData + + if let jsonString = String(data: jsonData, encoding: .utf8) { + Logger.shared.log("Trakt API Request Body: \(jsonString)", type: "Debug") + } } catch { + Logger.shared.log("Failed to serialize request body: \(error.localizedDescription)", type: "Error") completion(.failure(error)) return } + Logger.shared.log("Sending Trakt API request to: \(request.url?.absoluteString ?? "unknown")", type: "Debug") + let task = URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { + Logger.shared.log("Trakt API network error: \(error.localizedDescription)", type: "Error") completion(.failure(error)) return } guard let httpResponse = response as? HTTPURLResponse else { + Logger.shared.log("Trakt API: No HTTP response received", type: "Error") completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "No HTTP response"]))) return } + Logger.shared.log("Trakt API Response Status: \(httpResponse.statusCode)", type: "Debug") + + if let data = data, let responseString = String(data: data, encoding: .utf8) { + Logger.shared.log("Trakt API Response Body: \(responseString)", type: "Debug") + } + 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") + Logger.shared.log("Successfully updated watch status on Trakt for \(type)", type: "General") completion(.success(())) } else { - var errorMessage = "Unexpected status code: \(httpResponse.statusCode)" + var errorMessage = "HTTP \(httpResponse.statusCode)" if let data = data, - let errorJson = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - let error = errorJson["error"] as? String { - errorMessage = error + let errorJson = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { + if let error = errorJson["error"] as? String { + errorMessage = "\(errorMessage): \(error)" + } + if let errorDescription = errorJson["error_description"] as? String { + errorMessage = "\(errorMessage) - \(errorDescription)" + } } + Logger.shared.log("Trakt API Error: \(errorMessage)", type: "Error") completion(.failure(NSError(domain: "", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: errorMessage]))) } } diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index 07fbf25..bf6b098 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -1647,19 +1647,26 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele self.tryAniListUpdate() } - if let tmdbId = self.tmdbID { + if let tmdbId = self.tmdbID, tmdbId > 0 { + Logger.shared.log("Attempting Trakt update - TMDB ID: \(tmdbId), isMovie: \(self.isMovie), episode: \(self.episodeNumber), season: \(self.seasonNumber)", type: "Debug") + let traktMutation = TraktMutation() if self.isMovie { traktMutation.markAsWatched(type: "movie", tmdbID: tmdbId) { result in switch result { case .success: - Logger.shared.log("Successfully updated Trakt progress for movie", type: "General") + Logger.shared.log("Successfully updated Trakt progress for movie (TMDB: \(tmdbId))", type: "General") case .failure(let error): - Logger.shared.log("Failed to update Trakt progress: \(error.localizedDescription)", type: "Error") + Logger.shared.log("Failed to update Trakt progress for movie: \(error.localizedDescription)", type: "Error") } } } else { + guard self.episodeNumber > 0 && self.seasonNumber > 0 else { + Logger.shared.log("Invalid episode (\(self.episodeNumber)) or season (\(self.seasonNumber)) number for Trakt update", type: "Error") + return + } + traktMutation.markAsWatched( type: "episode", tmdbID: tmdbId, @@ -1668,12 +1675,14 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele ) { result in switch result { case .success: - Logger.shared.log("Successfully updated Trakt progress for episode \(self.episodeNumber)", type: "General") + Logger.shared.log("Successfully updated Trakt progress for episode \(self.episodeNumber) (TMDB: \(tmdbId))", type: "General") case .failure(let error): - Logger.shared.log("Failed to update Trakt progress: \(error.localizedDescription)", type: "Error") + Logger.shared.log("Failed to update Trakt progress for episode: \(error.localizedDescription)", type: "Error") } } } + } else { + Logger.shared.log("Skipping Trakt update - TMDB ID not set or invalid: \(self.tmdbID ?? -1)", type: "Warning") } } @@ -1831,6 +1840,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele @objc func seekForward() { let skipValue = UserDefaults.standard.double(forKey: "skipIncrement") + let finalSkip = skipValue > 0 ? skipValue : 10 currentTimeVal = min(currentTimeVal + finalSkip, duration) player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in diff --git a/Sora/Utils/MediaPlayer/VideoPlayer.swift b/Sora/Utils/MediaPlayer/VideoPlayer.swift index bd65523..7d182de 100644 --- a/Sora/Utils/MediaPlayer/VideoPlayer.swift +++ b/Sora/Utils/MediaPlayer/VideoPlayer.swift @@ -215,19 +215,26 @@ class VideoPlayerViewController: UIViewController { } } - if let tmdbId = self.tmdbID { + if let tmdbId = self.tmdbID, tmdbId > 0 { + Logger.shared.log("Attempting Trakt update - TMDB ID: \(tmdbId), isMovie: \(self.isMovie), episode: \(self.episodeNumber), season: \(self.seasonNumber)", type: "Debug") + let traktMutation = TraktMutation() if self.isMovie { traktMutation.markAsWatched(type: "movie", tmdbID: tmdbId) { result in switch result { case .success: - Logger.shared.log("Updated Trakt progress for movie", type: "General") + Logger.shared.log("Updated Trakt progress for movie (TMDB: \(tmdbId))", type: "General") case .failure(let error): - Logger.shared.log("Could not update Trakt progress: \(error.localizedDescription)", type: "Error") + Logger.shared.log("Could not update Trakt progress for movie: \(error.localizedDescription)", type: "Error") } } } else { + guard self.episodeNumber > 0 && self.seasonNumber > 0 else { + Logger.shared.log("Invalid episode (\(self.episodeNumber)) or season (\(self.seasonNumber)) number for Trakt update", type: "Error") + return + } + traktMutation.markAsWatched( type: "episode", tmdbID: tmdbId, @@ -236,12 +243,14 @@ class VideoPlayerViewController: UIViewController { ) { result in switch result { case .success: - Logger.shared.log("Updated Trakt progress for Episode \(self.episodeNumber)", type: "General") + Logger.shared.log("Updated Trakt progress for Episode \(self.episodeNumber) (TMDB: \(tmdbId))", type: "General") case .failure(let error): - Logger.shared.log("Could not update Trakt progress: \(error.localizedDescription)", type: "Error") + Logger.shared.log("Could not update Trakt progress for episode: \(error.localizedDescription)", type: "Error") } } } + } else { + Logger.shared.log("Skipping Trakt update - TMDB ID not set or invalid: \(self.tmdbID ?? -1)", type: "Warning") } } } diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 31d847d..66155ba 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -804,12 +804,12 @@ struct MediaInfoView: View { let total = UserDefaults.standard.double(forKey: totalTimeKey) let progress = total > 0 ? last/total : 0 let watchedEp = ep.number - + if progress <= 0.9 { UserDefaults.standard.set(99999999.0, forKey: lastPlayedKey) UserDefaults.standard.set(99999999.0, forKey: totalTimeKey) DropManager.shared.showDrop(title: "Marked as Watched", subtitle: "", duration: 1.0, icon: UIImage(systemName: "checkmark.circle.fill")) - + if let listID = itemID, listID > 0 { AniListMutation().updateAnimeProgress(animeId: listID, episodeNumber: watchedEp, status: "CURRENT") { result in switch result { @@ -824,7 +824,7 @@ struct MediaInfoView: View { UserDefaults.standard.set(0.0, forKey: lastPlayedKey) UserDefaults.standard.set(0.0, forKey: totalTimeKey) DropManager.shared.showDrop(title: "Progress Reset", subtitle: "", duration: 1.0, icon: UIImage(systemName: "arrow.counterclockwise")) - + if let listID = itemID, listID > 0 { AniListMutation().updateAnimeProgress(animeId: listID, episodeNumber: 0, status: "CURRENT") { _ in } } @@ -1204,7 +1204,6 @@ struct MediaInfoView: View { } } } - private func fetchAniListIDForSync() { let cleaned = cleanTitle(title) @@ -1225,76 +1224,73 @@ struct MediaInfoView: View { func fetchMetadataIDIfNeeded() { let order = metadataProvidersOrder let cleanedTitle = cleanTitle(title) - + itemID = nil tmdbID = nil activeProvider = nil isError = false - - func fetchAniList(completion: @escaping (Bool) -> Void) { - fetchItemID(byTitle: cleanedTitle) { result in + + var aniListCompleted = false + var tmdbCompleted = false + var aniListSuccess = false + var tmdbSuccess = false + + func checkCompletion() { + guard aniListCompleted && tmdbCompleted else { return } + + let primaryProvider = order.first ?? "AniList" + + if primaryProvider == "AniList" && aniListSuccess { + activeProvider = "AniList" + UserDefaults.standard.set("AniList", forKey: "metadataProviders") + } else if primaryProvider == "TMDB" && tmdbSuccess { + activeProvider = "TMDB" + UserDefaults.standard.set("TMDB", forKey: "metadataProviders") + } else if aniListSuccess { + activeProvider = "AniList" + UserDefaults.standard.set("AniList", forKey: "metadataProviders") + } else if tmdbSuccess { + activeProvider = "TMDB" + UserDefaults.standard.set("TMDB", forKey: "metadataProviders") + } else { + isError = true + } + } + + fetchItemID(byTitle: cleanedTitle) { result in + DispatchQueue.main.async { + aniListCompleted = true switch result { case .success(let id): - DispatchQueue.main.async { - self.itemID = id - self.activeProvider = "AniList" - UserDefaults.standard.set("AniList", forKey: "metadataProviders") - - tmdbFetcher.fetchBestMatchID(for: cleanedTitle) { tmdbId, tmdbType in - DispatchQueue.main.async { - guard let tmdbId = tmdbId, let tmdbType = tmdbType else { - completion(true) - return - } - self.tmdbID = tmdbId - self.tmdbType = tmdbType - self.fetchTMDBPosterImageAndSet() - completion(true) - } - } - } - + self.itemID = id + aniListSuccess = true + Logger.shared.log("Successfully fetched AniList ID: \(id)", type: "Debug") case .failure(let error): - Logger.shared.log("Failed to fetch AniList ID for tracking: \(error)", type: "Error") - completion(false) + Logger.shared.log("Failed to fetch AniList ID: \(error)", type: "Debug") } + checkCompletion() } } - - func tryProviders(_ index: Int) { - guard index < order.count else { - isError = true - return - } - - let provider = order[index] - switch provider { - case "AniList": - fetchAniList { success in - if !success { - tryProviders(index + 1) + + tmdbFetcher.fetchBestMatchID(for: cleanedTitle) { id, type in + DispatchQueue.main.async { + tmdbCompleted = true + if let id = id, let type = type { + self.tmdbID = id + self.tmdbType = type + tmdbSuccess = true + Logger.shared.log("Successfully fetched TMDB ID: \(id) (type: \(type.rawValue))", type: "Debug") + + if self.activeProvider != "TMDB" { + self.fetchTMDBPosterImageAndSet() } + } else { + Logger.shared.log("Failed to fetch TMDB ID", type: "Debug") } - case "TMDB": - tmdbFetcher.fetchBestMatchID(for: cleanedTitle) { id, type in - DispatchQueue.main.async { - if let id = id, let type = type { - self.tmdbID = id - self.tmdbType = type - self.activeProvider = "TMDB" - UserDefaults.standard.set("TMDB", forKey: "metadataProviders") - self.fetchTMDBPosterImageAndSet() - } else { - tryProviders(index + 1) - } - } - } - default: - tryProviders(index + 1) + checkCompletion() } } - - tryProviders(0) + fetchAniListIDForSync() } @@ -1560,6 +1556,8 @@ struct MediaInfoView: View { } private func presentDefaultPlayer(url: String, fullURL: String, subtitles: String?, headers: [String:String]?) { + let isMovie = tmdbType == .movie + let videoPlayerViewController = VideoPlayerViewController(module: module) videoPlayerViewController.headers = headers videoPlayerViewController.streamUrl = url @@ -1570,6 +1568,9 @@ struct MediaInfoView: View { videoPlayerViewController.mediaTitle = title videoPlayerViewController.subtitles = subtitles ?? "" videoPlayerViewController.aniListID = itemID ?? 0 + videoPlayerViewController.tmdbID = tmdbID + videoPlayerViewController.isMovie = isMovie + videoPlayerViewController.seasonNumber = selectedSeason + 1 videoPlayerViewController.modalPresentationStyle = .fullScreen if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, @@ -1589,6 +1590,7 @@ struct MediaInfoView: View { } guard self.activeFetchID == fetchID else { return } + let isMovie = tmdbType == .movie let customMediaPlayer = CustomMediaPlayerViewController( module: module, @@ -1604,6 +1606,8 @@ struct MediaInfoView: View { headers: headers ?? nil ) customMediaPlayer.seasonNumber = selectedSeason + 1 + customMediaPlayer.tmdbID = tmdbID + customMediaPlayer.isMovie = isMovie customMediaPlayer.modalPresentationStyle = .fullScreen Logger.shared.log("Opening custom media player with url: \(url)") diff --git a/Sora/Views/SplashScreenView.swift b/Sora/Views/SplashScreenView.swift index 01ec336..af37f9f 100644 --- a/Sora/Views/SplashScreenView.swift +++ b/Sora/Views/SplashScreenView.swift @@ -24,18 +24,13 @@ struct SplashScreenView: View { .cornerRadius(24) .scaleEffect(isAnimating ? 1.2 : 1.0) .opacity(isAnimating ? 1.0 : 0.0) - - Text("Sora") - .font(.largeTitle) - .fontWeight(.bold) - .opacity(isAnimating ? 1.0 : 0.0) } .onAppear { withAnimation(.easeIn(duration: 0.5)) { isAnimating = true } - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { withAnimation(.easeOut(duration: 0.5)) { showMainApp = true }