From d4bbf8770334ecd6b332f7360d0392425f1320ba Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Sun, 25 May 2025 13:05:53 +0200 Subject: [PATCH] test --- .../TMDB/TMDBEpisodeMetadata.swift | 38 +++++ .../EpisodeCell/EpisodeCell.swift | 134 +++++++++++++++++- Sora/Views/MediaInfoView/MediaInfoView.swift | 91 ++++++++++++ Sulfur.xcodeproj/project.pbxproj | 12 ++ 4 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 Sora/Tracking Services/TMDB/TMDBEpisodeMetadata.swift diff --git a/Sora/Tracking Services/TMDB/TMDBEpisodeMetadata.swift b/Sora/Tracking Services/TMDB/TMDBEpisodeMetadata.swift new file mode 100644 index 0000000..00922aa --- /dev/null +++ b/Sora/Tracking Services/TMDB/TMDBEpisodeMetadata.swift @@ -0,0 +1,38 @@ +// +// TMDBEpisodeMetadata.swift +// Sora +// +// Created by Francesco on 05/01/25. +// + +import Foundation + +struct TMDBEpisodeMetadata: Codable { + let title: String + let imageUrl: String + let tmdbId: Int + let seasonNumber: Int + let episodeNumber: Int + let cacheDate: Date + + var cacheKey: String { + return "tmdb_\(tmdbId)_s\(seasonNumber)_e\(episodeNumber)" + } + + init(title: String, imageUrl: String, tmdbId: Int, seasonNumber: Int, episodeNumber: Int) { + self.title = title + self.imageUrl = imageUrl + self.tmdbId = tmdbId + self.seasonNumber = seasonNumber + self.episodeNumber = episodeNumber + self.cacheDate = Date() + } + + func toData() -> Data? { + return try? JSONEncoder().encode(self) + } + + static func fromData(_ data: Data) -> TMDBEpisodeMetadata? { + return try? JSONDecoder().decode(TMDBEpisodeMetadata.self, from: data) + } +} diff --git a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift index f87051f..e6bb1e3 100644 --- a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift +++ b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift @@ -26,6 +26,7 @@ struct EpisodeCell: View { var module: ScrapingModule var parentTitle: String var showPosterURL: String? + var tmdbID: Int? var isMultiSelectMode: Bool = false var isSelected: Bool = false @@ -74,6 +75,7 @@ struct EpisodeCell: View { init(episodeIndex: Int, episode: String, episodeID: Int, progress: Double, itemID: Int, totalEpisodes: Int? = nil, defaultBannerImage: String = "", module: ScrapingModule, parentTitle: String, showPosterURL: String? = nil, + tmdbID: Int? = nil, isMultiSelectMode: Bool = false, isSelected: Bool = false, onSelectionChanged: ((Bool) -> Void)? = nil, onTap: @escaping (String) -> Void, onMarkAllPrevious: @escaping () -> Void) { @@ -83,6 +85,7 @@ struct EpisodeCell: View { self.progress = progress self.itemID = itemID self.totalEpisodes = totalEpisodes + self.tmdbID = tmdbID let isLightMode = (UserDefaults.standard.string(forKey: "selectedAppearance") == "light") || ((UserDefaults.standard.string(forKey: "selectedAppearance") == "system") && @@ -126,6 +129,10 @@ struct EpisodeCell: View { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { fetchAnimeEpisodeDetails() } + } else if tmdbID != nil { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + fetchTMDBEpisodeDetails() + } } else { isLoading = false } @@ -525,6 +532,28 @@ struct EpisodeCell: View { } private func fetchEpisodeDetails() { + if let tmdbID = tmdbID, + MetadataCacheManager.shared.isCachingEnabled && + (UserDefaults.standard.object(forKey: "fetchEpisodeMetadata") == nil || + UserDefaults.standard.bool(forKey: "fetchEpisodeMetadata")) { + + let seasonNumber = 1 + let episodeNumber = episodeID + 1 + let cacheKey = "tmdb_\(tmdbID)_s\(seasonNumber)_e\(episodeNumber)" + + if let cachedData = MetadataCacheManager.shared.getMetadata(forKey: cacheKey), + let metadata = TMDBEpisodeMetadata.fromData(cachedData) { + + DispatchQueue.main.async { + self.episodeTitle = metadata.title + self.episodeImageUrl = metadata.imageUrl + self.isLoading = false + self.loadedFromCache = true + } + return + } + } + if MetadataCacheManager.shared.isCachingEnabled && (UserDefaults.standard.object(forKey: "fetchEpisodeMetadata") == nil || UserDefaults.standard.bool(forKey: "fetchEpisodeMetadata")) { @@ -544,7 +573,13 @@ struct EpisodeCell: View { } } - fetchAnimeEpisodeDetails() + if let type = module.metadata.type?.lowercased(), type == "anime" { + fetchAnimeEpisodeDetails() + } else if tmdbID != nil { + fetchTMDBEpisodeDetails() + } else { + isLoading = false + } } private func fetchAnimeEpisodeDetails() { @@ -659,6 +694,103 @@ struct EpisodeCell: View { }.resume() } + private func fetchTMDBEpisodeDetails() { + guard let tmdbID = tmdbID else { + isLoading = false + return + } + let seasonNumber = 1 + let episodeNumber = episodeID + 1 + + guard let url = URL(string: "https://api.themoviedb.org/3/tv/\(tmdbID)/season/\(seasonNumber)/episode/\(episodeNumber)?api_key=eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI3MzhiNGVkZDBhMTU2Y2MxMjZkYzRhNGI4YWVhNGFjYSIsIm5iZiI6MTc0MTE3MzcwMi43ODcwMDAyLCJzdWIiOiI2N2M4MzNjNmQ3NDE5Y2RmZDg2ZTJkZGYiLCJzY29wZXMiOlsiYXBpX3JlYWQiXSwidmVyc2lvbiI6MX0.Gfe7F-8CWJXgONv34mg3jHXfL6Bxbj-hAYf9fYi9CkE") else { + isLoading = false + Logger.shared.log("Invalid TMDB URL for show ID: \(tmdbID)", type: "Error") + return + } + + if retryAttempts > 0 { + Logger.shared.log("Retrying TMDB episode details fetch (attempt \(retryAttempts)/\(maxRetryAttempts))", type: "Debug") + } + + URLSession.custom.dataTask(with: url) { data, response, error in + if let error = error { + Logger.shared.log("Failed to fetch TMDB episode details: \(error)", type: "Error") + self.handleFetchFailure(error: error) + return + } + + guard let data = data else { + self.handleFetchFailure(error: NSError(domain: "com.sora.episode", code: 1, userInfo: [NSLocalizedDescriptionKey: "No data received"])) + return + } + + do { + guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { + self.handleFetchFailure(error: NSError(domain: "com.sora.episode", code: 2, userInfo: [NSLocalizedDescriptionKey: "Invalid JSON format"])) + return + } + + var title = "" + var image = "" + var missingFields: [String] = [] + + if let episodeTitle = json["name"] as? String, !episodeTitle.isEmpty { + title = episodeTitle + } else { + missingFields.append("name") + } + + if let stillPath = json["still_path"] as? String, !stillPath.isEmpty { + image = "https://image.tmdb.org/t/p/w500\(stillPath)" + } else { + missingFields.append("still_path") + } + + if !missingFields.isEmpty { + Logger.shared.log("TMDB Episode \(episodeNumber) missing fields: \(missingFields.joined(separator: ", "))", type: "Warning") + } + + // Cache TMDB episode metadata + if MetadataCacheManager.shared.isCachingEnabled && (!title.isEmpty || !image.isEmpty) { + let metadata = TMDBEpisodeMetadata( + title: title, + imageUrl: image, + tmdbId: tmdbID, + seasonNumber: seasonNumber, + episodeNumber: episodeNumber + ) + + if let metadataData = metadata.toData() { + MetadataCacheManager.shared.storeMetadata( + metadataData, + forKey: metadata.cacheKey + ) + } + } + + DispatchQueue.main.async { + self.isLoading = false + self.retryAttempts = 0 + + if UserDefaults.standard.object(forKey: "fetchEpisodeMetadata") == nil + || UserDefaults.standard.bool(forKey: "fetchEpisodeMetadata") { + self.episodeTitle = title + + if !image.isEmpty { + self.episodeImageUrl = image + } + } + } + } catch { + Logger.shared.log("TMDB JSON parsing error: \(error.localizedDescription)", type: "Error") + DispatchQueue.main.async { + self.isLoading = false + self.retryAttempts = 0 + } + } + }.resume() + } + private func handleFetchFailure(error: Error) { Logger.shared.log("Episode details fetch error: \(error.localizedDescription)", type: "Error") diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 6535427..b6bdeb8 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -122,6 +122,7 @@ struct MediaInfoView: View { itemID = savedID Logger.shared.log("Using custom AniList ID: \(savedID)", type: "Debug") } else { + // Try AniList first for anime fetchItemID(byTitle: cleanTitle(title)) { result in switch result { case .success(let id): @@ -131,6 +132,16 @@ struct MediaInfoView: View { AnalyticsManager.shared.sendEvent(event: "error", additionalData: ["error": error, "message": "Failed to fetch AniList ID"]) } } + + // Also try TMDB for series/movies + fetchTMDBID(byTitle: cleanTitle(title)) { result in + switch result { + case .success(let id): + tmdbID = id + case .failure(let error): + Logger.shared.log("Failed to fetch TMDB ID: \(error)") + } + } } selectedRange = 0..) -> Void) { + guard let encodedTitle = title.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let url = URL(string: "https://api.themoviedb.org/3/search/tv?api_key=eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI3MzhiNGVkZDBhMTU2Y2MxMjZkYzRhNGI4YWVhNGFjYSIsIm5iZiI6MTc0MTE3MzcwMi43ODcwMDAyLCJzdWIiOiI2N2M4MzNjNmQ3NDE5Y2RmZDg2ZTJkZGYiLCJzY29wZXMiOlsiYXBpX3JlYWQiXSwidmVyc2lvbiI6MX0.Gfe7F-8CWJXgONv34mg3jHXfL6Bxbj-hAYf9fYi9CkE&query=\(encodedTitle)") else { + completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]))) + return + } + + URLSession.custom.dataTask(with: url) { data, _, error in + if let error = error { + completion(.failure(error)) + return + } + + guard let data = data else { + completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data received"]))) + return + } + + do { + if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], + let results = json["results"] as? [[String: Any]], + let firstResult = results.first, + let id = firstResult["id"] as? Int { + completion(.success(id)) + } else { + let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "No TMDB results found"]) + completion(.failure(error)) + } + } catch { + completion(.failure(error)) + } + }.resume() + } + private func showCustomIDAlert() { let alert = UIAlertController(title: "Set Custom AniList ID", message: "Enter the AniList ID for this media", preferredStyle: .alert) @@ -1123,6 +1186,34 @@ struct MediaInfoView: View { } } + private func showCustomTMDBIDAlert() { + let alert = UIAlertController(title: "Set Custom TMDB ID", message: "Enter the TMDB ID for this TV show", preferredStyle: .alert) + + alert.addTextField { textField in + textField.placeholder = "TMDB ID" + textField.keyboardType = .numberPad + if let tmdbID = tmdbID { + textField.text = "\(tmdbID)" + } + } + + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + alert.addAction(UIAlertAction(title: "Save", style: .default) { _ in + if let text = alert.textFields?.first?.text, + let id = Int(text) { + tmdbID = id + UserDefaults.standard.set(id, forKey: "custom_tmdb_id_\(href)") + Logger.shared.log("Set custom TMDB ID: \(id)", type: "General") + } + }) + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first, + let rootVC = window.rootViewController { + findTopViewController.findViewController(rootVC).present(alert, animated: true) + } + } + private func selectEpisodeRange(start: Int, end: Int) { selectedEpisodes.removeAll() for episodeNumber in start...end { diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index b250456..4da8b48 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 132AF1232D9995C300A0140B /* JSController-Details.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1222D9995C300A0140B /* JSController-Details.swift */; }; 132AF1252D9995F900A0140B /* JSController-Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1242D9995F900A0140B /* JSController-Search.swift */; }; 132FC5B32DE31DAE009A80F7 /* SettingsViewAlternateAppIconPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132FC5B22DE31DAD009A80F7 /* SettingsViewAlternateAppIconPicker.swift */; }; + 132FC5B82DE33051009A80F7 /* TMDBEpisodeMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132FC5B72DE33051009A80F7 /* TMDBEpisodeMetadata.swift */; }; 133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6D2D2BE2500075467E /* SoraApp.swift */; }; 133D7C702D2BE2500075467E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6F2D2BE2500075467E /* ContentView.swift */; }; 133D7C722D2BE2520075467E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 133D7C712D2BE2520075467E /* Assets.xcassets */; }; @@ -98,6 +99,7 @@ 132AF1222D9995C300A0140B /* JSController-Details.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Details.swift"; sourceTree = ""; }; 132AF1242D9995F900A0140B /* JSController-Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Search.swift"; sourceTree = ""; }; 132FC5B22DE31DAD009A80F7 /* SettingsViewAlternateAppIconPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewAlternateAppIconPicker.swift; sourceTree = ""; }; + 132FC5B72DE33051009A80F7 /* TMDBEpisodeMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TMDBEpisodeMetadata.swift; sourceTree = ""; }; 133D7C6A2D2BE2500075467E /* Sulfur.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sulfur.app; sourceTree = BUILT_PRODUCTS_DIR; }; 133D7C6D2D2BE2500075467E /* SoraApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraApp.swift; sourceTree = ""; }; 133D7C6F2D2BE2500075467E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -181,6 +183,7 @@ 13103E802D589D6C000F0673 /* Tracking Services */ = { isa = PBXGroup; children = ( + 132FC5B42DE33042009A80F7 /* TMDB */, 13E62FBF2DABC3A20007E259 /* Trakt */, 13103E812D589D77000F0673 /* AniList */, ); @@ -213,6 +216,14 @@ path = Analytics; sourceTree = ""; }; + 132FC5B42DE33042009A80F7 /* TMDB */ = { + isa = PBXGroup; + children = ( + 132FC5B72DE33051009A80F7 /* TMDBEpisodeMetadata.swift */, + ); + path = TMDB; + sourceTree = ""; + }; 133D7C612D2BE2500075467E = { isa = PBXGroup; children = ( @@ -616,6 +627,7 @@ 130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */, 1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */, 13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */, + 132FC5B82DE33051009A80F7 /* TMDBEpisodeMetadata.swift in Sources */, 133D7C932D2BE2640075467E /* Modules.swift in Sources */, 13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */, 133D7C702D2BE2500075467E /* ContentView.swift in Sources */,