please trakt please 🙏

This commit is contained in:
cranci1 2025-06-14 16:19:10 +02:00
parent ffeddb37e6
commit 51dcae1a54
5 changed files with 138 additions and 91 deletions

View file

@ -25,29 +25,36 @@ class TraktMutation {
guard status == errSecSuccess, guard status == errSecSuccess,
let tokenData = item as? Data, let tokenData = item as? Data,
let token = String(data: tokenData, encoding: .utf8) else { let token = String(data: tokenData, encoding: .utf8) else {
return nil return nil
} }
return token return token
} }
func markAsWatched(type: String, tmdbID: Int, episodeNumber: Int? = nil, seasonNumber: Int? = nil, completion: @escaping (Result<Void, Error>) -> Void) { func markAsWatched(type: String, tmdbID: Int, episodeNumber: Int? = nil, seasonNumber: Int? = nil, completion: @escaping (Result<Void, Error>) -> Void) {
if let sendTraktUpdates = UserDefaults.standard.object(forKey: "sendTraktUpdates") as? Bool, let sendTraktUpdates = UserDefaults.standard.object(forKey: "sendTraktUpdates") as? Bool ?? true
sendTraktUpdates == false { 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 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 { 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"]))) completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Access token not found"])))
Logger.shared.log("Trakt Access token not found", type: "Error")
return return
} }
Logger.shared.log("Found Trakt access token, proceeding with API call", type: "Debug")
let endpoint = "/sync/history" let endpoint = "/sync/history"
let watchedAt = ISO8601DateFormatter().string(from: Date()) let watchedAt = ISO8601DateFormatter().string(from: Date())
let body: [String: Any] let body: [String: Any]
switch type { switch type {
case "movie": case "movie":
Logger.shared.log("Preparing movie watch request for TMDB ID: \(tmdbID)", type: "Debug")
body = [ body = [
"movies": [ "movies": [
[ [
@ -59,10 +66,13 @@ class TraktMutation {
case "episode": case "episode":
guard let episode = episodeNumber, let season = seasonNumber else { 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 return
} }
Logger.shared.log("Preparing episode watch request - TMDB ID: \(tmdbID), Season: \(season), Episode: \(episode)", type: "Debug")
body = [ body = [
"shows": [ "shows": [
[ [
@ -83,6 +93,7 @@ class TraktMutation {
] ]
default: default:
Logger.shared.log("Invalid content type: \(type)", type: "Error")
completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid content type"]))) completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid content type"])))
return return
} }
@ -95,36 +106,54 @@ class TraktMutation {
request.setValue(TraktToken.clientID, forHTTPHeaderField: "trakt-api-key") request.setValue(TraktToken.clientID, forHTTPHeaderField: "trakt-api-key")
do { 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 { } catch {
Logger.shared.log("Failed to serialize request body: \(error.localizedDescription)", type: "Error")
completion(.failure(error)) completion(.failure(error))
return 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 let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error { if let error = error {
Logger.shared.log("Trakt API network error: \(error.localizedDescription)", type: "Error")
completion(.failure(error)) completion(.failure(error))
return return
} }
guard let httpResponse = response as? HTTPURLResponse else { 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"]))) completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "No HTTP response"])))
return 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 (200...299).contains(httpResponse.statusCode) {
if let data = data, let responseString = String(data: data, encoding: .utf8) { Logger.shared.log("Successfully updated watch status on Trakt for \(type)", type: "General")
Logger.shared.log("Trakt API Response: \(responseString)", type: "Debug")
}
Logger.shared.log("Successfully updated watch status on Trakt", type: "Debug")
completion(.success(())) completion(.success(()))
} else { } else {
var errorMessage = "Unexpected status code: \(httpResponse.statusCode)" var errorMessage = "HTTP \(httpResponse.statusCode)"
if let data = data, if let data = data,
let errorJson = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let errorJson = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
let error = errorJson["error"] as? String { if let error = errorJson["error"] as? String {
errorMessage = error 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]))) completion(.failure(NSError(domain: "", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: errorMessage])))
} }
} }

View file

@ -1647,19 +1647,26 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
self.tryAniListUpdate() 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() let traktMutation = TraktMutation()
if self.isMovie { if self.isMovie {
traktMutation.markAsWatched(type: "movie", tmdbID: tmdbId) { result in traktMutation.markAsWatched(type: "movie", tmdbID: tmdbId) { result in
switch result { switch result {
case .success: 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): 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 { } 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( traktMutation.markAsWatched(
type: "episode", type: "episode",
tmdbID: tmdbId, tmdbID: tmdbId,
@ -1668,12 +1675,14 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
) { result in ) { result in
switch result { switch result {
case .success: 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): 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() { @objc func seekForward() {
let skipValue = UserDefaults.standard.double(forKey: "skipIncrement") let skipValue = UserDefaults.standard.double(forKey: "skipIncrement")
let finalSkip = skipValue > 0 ? skipValue : 10 let finalSkip = skipValue > 0 ? skipValue : 10
currentTimeVal = min(currentTimeVal + finalSkip, duration) currentTimeVal = min(currentTimeVal + finalSkip, duration)
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in

View file

@ -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() let traktMutation = TraktMutation()
if self.isMovie { if self.isMovie {
traktMutation.markAsWatched(type: "movie", tmdbID: tmdbId) { result in traktMutation.markAsWatched(type: "movie", tmdbID: tmdbId) { result in
switch result { switch result {
case .success: 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): 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 { } 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( traktMutation.markAsWatched(
type: "episode", type: "episode",
tmdbID: tmdbId, tmdbID: tmdbId,
@ -236,12 +243,14 @@ class VideoPlayerViewController: UIViewController {
) { result in ) { result in
switch result { switch result {
case .success: 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): 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")
} }
} }
} }

View file

@ -804,12 +804,12 @@ struct MediaInfoView: View {
let total = UserDefaults.standard.double(forKey: totalTimeKey) let total = UserDefaults.standard.double(forKey: totalTimeKey)
let progress = total > 0 ? last/total : 0 let progress = total > 0 ? last/total : 0
let watchedEp = ep.number let watchedEp = ep.number
if progress <= 0.9 { if progress <= 0.9 {
UserDefaults.standard.set(99999999.0, forKey: lastPlayedKey) UserDefaults.standard.set(99999999.0, forKey: lastPlayedKey)
UserDefaults.standard.set(99999999.0, forKey: totalTimeKey) UserDefaults.standard.set(99999999.0, forKey: totalTimeKey)
DropManager.shared.showDrop(title: "Marked as Watched", subtitle: "", duration: 1.0, icon: UIImage(systemName: "checkmark.circle.fill")) DropManager.shared.showDrop(title: "Marked as Watched", subtitle: "", duration: 1.0, icon: UIImage(systemName: "checkmark.circle.fill"))
if let listID = itemID, listID > 0 { if let listID = itemID, listID > 0 {
AniListMutation().updateAnimeProgress(animeId: listID, episodeNumber: watchedEp, status: "CURRENT") { result in AniListMutation().updateAnimeProgress(animeId: listID, episodeNumber: watchedEp, status: "CURRENT") { result in
switch result { switch result {
@ -824,7 +824,7 @@ struct MediaInfoView: View {
UserDefaults.standard.set(0.0, forKey: lastPlayedKey) UserDefaults.standard.set(0.0, forKey: lastPlayedKey)
UserDefaults.standard.set(0.0, forKey: totalTimeKey) UserDefaults.standard.set(0.0, forKey: totalTimeKey)
DropManager.shared.showDrop(title: "Progress Reset", subtitle: "", duration: 1.0, icon: UIImage(systemName: "arrow.counterclockwise")) DropManager.shared.showDrop(title: "Progress Reset", subtitle: "", duration: 1.0, icon: UIImage(systemName: "arrow.counterclockwise"))
if let listID = itemID, listID > 0 { if let listID = itemID, listID > 0 {
AniListMutation().updateAnimeProgress(animeId: listID, episodeNumber: 0, status: "CURRENT") { _ in } AniListMutation().updateAnimeProgress(animeId: listID, episodeNumber: 0, status: "CURRENT") { _ in }
} }
@ -1204,7 +1204,6 @@ struct MediaInfoView: View {
} }
} }
} }
private func fetchAniListIDForSync() { private func fetchAniListIDForSync() {
let cleaned = cleanTitle(title) let cleaned = cleanTitle(title)
@ -1225,76 +1224,73 @@ struct MediaInfoView: View {
func fetchMetadataIDIfNeeded() { func fetchMetadataIDIfNeeded() {
let order = metadataProvidersOrder let order = metadataProvidersOrder
let cleanedTitle = cleanTitle(title) let cleanedTitle = cleanTitle(title)
itemID = nil itemID = nil
tmdbID = nil tmdbID = nil
activeProvider = nil activeProvider = nil
isError = false isError = false
func fetchAniList(completion: @escaping (Bool) -> Void) { var aniListCompleted = false
fetchItemID(byTitle: cleanedTitle) { result in 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 { switch result {
case .success(let id): case .success(let id):
DispatchQueue.main.async { self.itemID = id
self.itemID = id aniListSuccess = true
self.activeProvider = "AniList" Logger.shared.log("Successfully fetched AniList ID: \(id)", type: "Debug")
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)
}
}
}
case .failure(let error): case .failure(let error):
Logger.shared.log("Failed to fetch AniList ID for tracking: \(error)", type: "Error") Logger.shared.log("Failed to fetch AniList ID: \(error)", type: "Debug")
completion(false)
} }
checkCompletion()
} }
} }
func tryProviders(_ index: Int) { tmdbFetcher.fetchBestMatchID(for: cleanedTitle) { id, type in
guard index < order.count else { DispatchQueue.main.async {
isError = true tmdbCompleted = true
return if let id = id, let type = type {
} self.tmdbID = id
self.tmdbType = type
let provider = order[index] tmdbSuccess = true
switch provider { Logger.shared.log("Successfully fetched TMDB ID: \(id) (type: \(type.rawValue))", type: "Debug")
case "AniList":
fetchAniList { success in if self.activeProvider != "TMDB" {
if !success { self.fetchTMDBPosterImageAndSet()
tryProviders(index + 1)
} }
} else {
Logger.shared.log("Failed to fetch TMDB ID", type: "Debug")
} }
case "TMDB": checkCompletion()
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)
} }
} }
tryProviders(0)
fetchAniListIDForSync() fetchAniListIDForSync()
} }
@ -1560,6 +1556,8 @@ struct MediaInfoView: View {
} }
private func presentDefaultPlayer(url: String, fullURL: String, subtitles: String?, headers: [String:String]?) { private func presentDefaultPlayer(url: String, fullURL: String, subtitles: String?, headers: [String:String]?) {
let isMovie = tmdbType == .movie
let videoPlayerViewController = VideoPlayerViewController(module: module) let videoPlayerViewController = VideoPlayerViewController(module: module)
videoPlayerViewController.headers = headers videoPlayerViewController.headers = headers
videoPlayerViewController.streamUrl = url videoPlayerViewController.streamUrl = url
@ -1570,6 +1568,9 @@ struct MediaInfoView: View {
videoPlayerViewController.mediaTitle = title videoPlayerViewController.mediaTitle = title
videoPlayerViewController.subtitles = subtitles ?? "" videoPlayerViewController.subtitles = subtitles ?? ""
videoPlayerViewController.aniListID = itemID ?? 0 videoPlayerViewController.aniListID = itemID ?? 0
videoPlayerViewController.tmdbID = tmdbID
videoPlayerViewController.isMovie = isMovie
videoPlayerViewController.seasonNumber = selectedSeason + 1
videoPlayerViewController.modalPresentationStyle = .fullScreen videoPlayerViewController.modalPresentationStyle = .fullScreen
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
@ -1589,6 +1590,7 @@ struct MediaInfoView: View {
} }
guard self.activeFetchID == fetchID else { return } guard self.activeFetchID == fetchID else { return }
let isMovie = tmdbType == .movie
let customMediaPlayer = CustomMediaPlayerViewController( let customMediaPlayer = CustomMediaPlayerViewController(
module: module, module: module,
@ -1604,6 +1606,8 @@ struct MediaInfoView: View {
headers: headers ?? nil headers: headers ?? nil
) )
customMediaPlayer.seasonNumber = selectedSeason + 1 customMediaPlayer.seasonNumber = selectedSeason + 1
customMediaPlayer.tmdbID = tmdbID
customMediaPlayer.isMovie = isMovie
customMediaPlayer.modalPresentationStyle = .fullScreen customMediaPlayer.modalPresentationStyle = .fullScreen
Logger.shared.log("Opening custom media player with url: \(url)") Logger.shared.log("Opening custom media player with url: \(url)")

View file

@ -24,18 +24,13 @@ struct SplashScreenView: View {
.cornerRadius(24) .cornerRadius(24)
.scaleEffect(isAnimating ? 1.2 : 1.0) .scaleEffect(isAnimating ? 1.2 : 1.0)
.opacity(isAnimating ? 1.0 : 0.0) .opacity(isAnimating ? 1.0 : 0.0)
Text("Sora")
.font(.largeTitle)
.fontWeight(.bold)
.opacity(isAnimating ? 1.0 : 0.0)
} }
.onAppear { .onAppear {
withAnimation(.easeIn(duration: 0.5)) { withAnimation(.easeIn(duration: 0.5)) {
isAnimating = true isAnimating = true
} }
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
withAnimation(.easeOut(duration: 0.5)) { withAnimation(.easeOut(duration: 0.5)) {
showMainApp = true showMainApp = true
} }