mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +00:00
test
This commit is contained in:
parent
fe097bfd14
commit
d4bbf87703
4 changed files with 274 additions and 1 deletions
38
Sora/Tracking Services/TMDB/TMDBEpisodeMetadata.swift
Normal file
38
Sora/Tracking Services/TMDB/TMDBEpisodeMetadata.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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..<episodeChunkSize
|
||||
|
|
@ -276,6 +287,12 @@ struct MediaInfoView: View {
|
|||
Label("Set Custom AniList ID", systemImage: "number")
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
showCustomTMDBIDAlert()
|
||||
}) {
|
||||
Label("Set Custom TMDB ID", systemImage: "tv")
|
||||
}
|
||||
|
||||
if let customID = customAniListID {
|
||||
Button(action: {
|
||||
customAniListID = nil
|
||||
|
|
@ -303,6 +320,16 @@ struct MediaInfoView: View {
|
|||
}
|
||||
}
|
||||
|
||||
if let id = tmdbID {
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://www.themoviedb.org/tv/\(id)") {
|
||||
openSafariViewController(with: url.absoluteString)
|
||||
}
|
||||
}) {
|
||||
Label("Open in TMDB", systemImage: "tv")
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button(action: {
|
||||
|
|
@ -463,6 +490,7 @@ struct MediaInfoView: View {
|
|||
module: module,
|
||||
parentTitle: title,
|
||||
showPosterURL: imageUrl,
|
||||
tmdbID: tmdbID,
|
||||
isMultiSelectMode: isMultiSelectMode,
|
||||
isSelected: selectedEpisodes.contains(ep.number),
|
||||
onSelectionChanged: { isSelected in
|
||||
|
|
@ -543,6 +571,7 @@ struct MediaInfoView: View {
|
|||
module: module,
|
||||
parentTitle: title,
|
||||
showPosterURL: imageUrl,
|
||||
tmdbID: tmdbID,
|
||||
isMultiSelectMode: isMultiSelectMode,
|
||||
isSelected: selectedEpisodes.contains(ep.number),
|
||||
onSelectionChanged: { isSelected in
|
||||
|
|
@ -1094,6 +1123,40 @@ struct MediaInfoView: View {
|
|||
}.resume()
|
||||
}
|
||||
|
||||
private func fetchTMDBID(byTitle title: String, completion: @escaping (Result<Int, Error>) -> 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 {
|
||||
|
|
|
|||
|
|
@ -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 = "<group>"; };
|
||||
132AF1242D9995F900A0140B /* JSController-Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Search.swift"; sourceTree = "<group>"; };
|
||||
132FC5B22DE31DAD009A80F7 /* SettingsViewAlternateAppIconPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewAlternateAppIconPicker.swift; sourceTree = "<group>"; };
|
||||
132FC5B72DE33051009A80F7 /* TMDBEpisodeMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TMDBEpisodeMetadata.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
133D7C6F2D2BE2500075467E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -181,6 +183,7 @@
|
|||
13103E802D589D6C000F0673 /* Tracking Services */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132FC5B42DE33042009A80F7 /* TMDB */,
|
||||
13E62FBF2DABC3A20007E259 /* Trakt */,
|
||||
13103E812D589D77000F0673 /* AniList */,
|
||||
);
|
||||
|
|
@ -213,6 +216,14 @@
|
|||
path = Analytics;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132FC5B42DE33042009A80F7 /* TMDB */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132FC5B72DE33051009A80F7 /* TMDBEpisodeMetadata.swift */,
|
||||
);
|
||||
path = TMDB;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
|
|
|||
Loading…
Reference in a new issue