mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +00:00
please trakt please 🙏
This commit is contained in:
parent
ffeddb37e6
commit
51dcae1a54
5 changed files with 138 additions and 91 deletions
|
|
@ -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])))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue