diff --git a/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift index 21306e6..346e10c 100644 --- a/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift +++ b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift @@ -12,6 +12,9 @@ import MediaPlayer import AVFoundation import MarqueeLabel +private let introDBFetcher = IntroDBFetcher() +private let tmdbFetcher = TMDBFetcher() + class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDelegate, AVPlayerViewControllerDelegate { private var airplayButton: AVRoutePickerView! let module: ScrapingModule @@ -78,6 +81,12 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele } return UserDefaults.standard.bool(forKey: "autoplayNext") } + private var isIntroDBEnabled: Bool { + if UserDefaults.standard.object(forKey: "introDBEnabled") == nil { + return true + } + return UserDefaults.standard.bool(forKey: "introDBEnabled") + } private var pipController: AVPictureInPictureController? var portraitButtonVisibleConstraints: [NSLayoutConstraint] = [] @@ -411,6 +420,11 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele Logger.shared.log("Unable to fetch MAL ID: \(error)",type:"Error") } } + + // Fetch IntroDB data for TV shows + if !isMovie, let tmdbId = tmdbID, isIntroDBEnabled { + fetchIntroDBData(tmdbId: tmdbId) + } for control in controlsToHide { originalHiddenStates[control] = control.isHidden @@ -1697,7 +1711,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele let resp = try? JSONDecoder().decode(AniSkipResponse.self, from: d), resp.found, let interval = resp.results.first?.interval else { return } - + let range = CMTimeRange( start: CMTime(seconds: interval.startTime, preferredTimescale: 600), end: CMTime(seconds: interval.endTime, preferredTimescale: 600) @@ -1714,6 +1728,35 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele } }.resume() } + + private func fetchIntroDBData(tmdbId: Int) { + tmdbFetcher.fetchExternalIDs(for: tmdbId, type: .tv) { [weak self] imdbId in + guard let self = self, let imdbId = imdbId else { + Logger.shared.log("Failed to fetch IMDB ID for TMDB ID: \(tmdbId)", type: "Debug") + return + } + + introDBFetcher.fetchIntro(imdbId: imdbId, season: self.seasonNumber, episode: self.episodeNumber) { introResponse in + guard let intro = introResponse else { + Logger.shared.log("No intro data available for IMDB: \(imdbId), S\(self.seasonNumber)E\(self.episodeNumber)", type: "Debug") + return + } + + let range = CMTimeRange( + start: CMTime(seconds: intro.start_sec, preferredTimescale: 600), + end: CMTime(seconds: intro.end_sec, preferredTimescale: 600) + ) + + DispatchQueue.main.async { + self.skipIntervals.op = range // Use op for intro + if self.duration > 0 { + self.updateSegments() + } + Logger.shared.log("Loaded IntroDB data: start \(intro.start_sec)s, end \(intro.end_sec)s, confidence \(intro.confidence)", type: "Debug") + } + } + } + } func setupSkipButtons() { let introConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold) diff --git a/Sora/Tracking & Metadata/TMDB/IntroDB-FetchIntro.swift b/Sora/Tracking & Metadata/TMDB/IntroDB-FetchIntro.swift new file mode 100644 index 0000000..28953c1 --- /dev/null +++ b/Sora/Tracking & Metadata/TMDB/IntroDB-FetchIntro.swift @@ -0,0 +1,60 @@ +// +// IntroDB-FetchIntro.swift +// Sora +// +// Created by 686udjie on 29/12/25. +// + +import Foundation + +class IntroDBFetcher { + struct IntroResponse: Decodable { + let imdb_id: String + let season: Int + let episode: Int + let start_sec: Double + let end_sec: Double + let start_ms: Int + let end_ms: Int + let confidence: Double + } + + struct ErrorResponse: Decodable { + let error: String + } + + private let session = URLSession.custom + + func fetchIntro(imdbId: String, season: Int, episode: Int, completion: @escaping (IntroResponse?) -> Void) { + let urlString = "https://api.introdb.app/intro?imdb_id=\(imdbId)&season=\(season)&episode=\(episode)" + guard let url = URL(string: urlString) else { + completion(nil) + return + } + + session.dataTask(with: url) { data, response, error in + guard let data = data, error == nil else { + completion(nil) + return + } + + let httpResponse = response as? HTTPURLResponse + if httpResponse?.statusCode == 404 { + // No intro data available + completion(nil) + return + } + + do { + let intro = try JSONDecoder().decode(IntroResponse.self, from: data) + completion(intro) + } catch { + // Try to decode as error response + if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: data) { + Logger.shared.log("IntroDB error: \(errorResponse.error)", type: "Debug") + } + completion(nil) + } + }.resume() + } +} diff --git a/Sora/Tracking & Metadata/TMDB/TMDB-FetchID.swift b/Sora/Tracking & Metadata/TMDB/TMDB-FetchID.swift index 0112a6d..1350cca 100644 --- a/Sora/Tracking & Metadata/TMDB/TMDB-FetchID.swift +++ b/Sora/Tracking & Metadata/TMDB/TMDB-FetchID.swift @@ -101,4 +101,29 @@ class TMDBFetcher { } return dist[a.count][b.count] } + + func fetchExternalIDs(for id: Int, type: MediaType, completion: @escaping (String?) -> Void) { + let urlString = "https://api.themoviedb.org/3/\(type.rawValue)/\(id)/external_ids?api_key=\(apiKey)" + guard let url = URL(string: urlString) else { + completion(nil) + return + } + + session.dataTask(with: url) { data, _, error in + guard let data = data, error == nil else { + completion(nil) + return + } + do { + if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], + let imdbId = json["imdb_id"] as? String { + completion(imdbId) + } else { + completion(nil) + } + } catch { + completion(nil) + } + }.resume() + } } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift index 1e3df07..5625ca1 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift @@ -258,7 +258,8 @@ struct SettingsViewPlayer: View { @AppStorage("doubleTapSeekEnabled") private var doubleTapSeekEnabled: Bool = false @AppStorage("skipIntroOutroVisible") private var skipIntroOutroVisible: Bool = true @AppStorage("autoplayNext") private var autoplayNext: Bool = true - + @AppStorage("introDBEnabled") private var introDBEnabled: Bool = true + @AppStorage("videoQualityWiFi") private var wifiQuality: String = VideoQualityPreference.defaultWiFiPreference.rawValue @AppStorage("videoQualityCellular") private var cellularQuality: String = VideoQualityPreference.defaultCellularPreference.rawValue @@ -408,7 +409,13 @@ struct SettingsViewPlayer: View { SettingsToggleRow( icon: "forward.frame", title: NSLocalizedString("Show Skip Intro / Outro Buttons", comment: ""), - isOn: $skipIntroOutroVisible, + isOn: $skipIntroOutroVisible + ) + + SettingsToggleRow( + icon: "film", + title: NSLocalizedString("IntroDB", comment: ""), + isOn: $introDBEnabled, showDivider: false ) } diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 46fd674..8dec489 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -116,6 +116,7 @@ 13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */; }; 13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */; }; 13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */; }; + 146A48DE2F02F1980017D145 /* IntroDB-FetchIntro.swift in Sources */ = {isa = PBXBuildFile; fileRef = 146A48DD2F02F1930017D145 /* IntroDB-FetchIntro.swift */; }; 1E26E9E72DA9577900B9DC02 /* VolumeSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E26E9E62DA9577900B9DC02 /* VolumeSlider.swift */; }; 1E47859B2DEBC5960095BF2F /* AnilistMatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E47859A2DEBC5960095BF2F /* AnilistMatchView.swift */; }; 1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; }; @@ -245,6 +246,7 @@ 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPlayer.swift; sourceTree = ""; }; 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; }; 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = ""; }; + 146A48DD2F02F1930017D145 /* IntroDB-FetchIntro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntroDB-FetchIntro.swift"; sourceTree = ""; }; 1E26E9E62DA9577900B9DC02 /* VolumeSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeSlider.swift; sourceTree = ""; }; 1E47859A2DEBC5960095BF2F /* AnilistMatchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnilistMatchView.swift; sourceTree = ""; }; 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = ""; }; @@ -799,6 +801,7 @@ 138FE1CE2DEC9FFA00936D81 /* TMDB */ = { isa = PBXGroup; children = ( + 146A48DD2F02F1930017D145 /* IntroDB-FetchIntro.swift */, 138FE1CF2DECA00D00936D81 /* TMDB-FetchID.swift */, ); path = TMDB; @@ -1150,6 +1153,7 @@ 13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */, 13637B8A2DE0EA1100BDA2FC /* UserDefaults.swift in Sources */, 7260B66D2E32A8CB00365CDA /* OrphanedDownloadsView.swift in Sources */, + 146A48DE2F02F1980017D145 /* IntroDB-FetchIntro.swift in Sources */, 0457C59D2DE78267000AFBD9 /* BookmarkGridView.swift in Sources */, 0457C59E2DE78267000AFBD9 /* BookmarkLink.swift in Sources */, 0457C59F2DE78267000AFBD9 /* BookmarkGridItemView.swift in Sources */,