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,
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, Error>) -> 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])))
}
}

View file

@ -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

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()
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")
}
}
}

View file

@ -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)")

View file

@ -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
}