mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +00:00
add IntroDB (#261)
This commit is contained in:
parent
97b81186ae
commit
4344ee10fe
5 changed files with 142 additions and 3 deletions
|
|
@ -12,6 +12,9 @@ import MediaPlayer
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import MarqueeLabel
|
import MarqueeLabel
|
||||||
|
|
||||||
|
private let introDBFetcher = IntroDBFetcher()
|
||||||
|
private let tmdbFetcher = TMDBFetcher()
|
||||||
|
|
||||||
class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDelegate, AVPlayerViewControllerDelegate {
|
class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDelegate, AVPlayerViewControllerDelegate {
|
||||||
private var airplayButton: AVRoutePickerView!
|
private var airplayButton: AVRoutePickerView!
|
||||||
let module: ScrapingModule
|
let module: ScrapingModule
|
||||||
|
|
@ -78,6 +81,12 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
||||||
}
|
}
|
||||||
return UserDefaults.standard.bool(forKey: "autoplayNext")
|
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?
|
private var pipController: AVPictureInPictureController?
|
||||||
|
|
||||||
var portraitButtonVisibleConstraints: [NSLayoutConstraint] = []
|
var portraitButtonVisibleConstraints: [NSLayoutConstraint] = []
|
||||||
|
|
@ -411,6 +420,11 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
||||||
Logger.shared.log("Unable to fetch MAL ID: \(error)",type:"Error")
|
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 {
|
for control in controlsToHide {
|
||||||
originalHiddenStates[control] = control.isHidden
|
originalHiddenStates[control] = control.isHidden
|
||||||
|
|
@ -1697,7 +1711,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
||||||
let resp = try? JSONDecoder().decode(AniSkipResponse.self, from: d),
|
let resp = try? JSONDecoder().decode(AniSkipResponse.self, from: d),
|
||||||
resp.found,
|
resp.found,
|
||||||
let interval = resp.results.first?.interval else { return }
|
let interval = resp.results.first?.interval else { return }
|
||||||
|
|
||||||
let range = CMTimeRange(
|
let range = CMTimeRange(
|
||||||
start: CMTime(seconds: interval.startTime, preferredTimescale: 600),
|
start: CMTime(seconds: interval.startTime, preferredTimescale: 600),
|
||||||
end: CMTime(seconds: interval.endTime, preferredTimescale: 600)
|
end: CMTime(seconds: interval.endTime, preferredTimescale: 600)
|
||||||
|
|
@ -1714,6 +1728,35 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
||||||
}
|
}
|
||||||
}.resume()
|
}.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() {
|
func setupSkipButtons() {
|
||||||
let introConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold)
|
let introConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold)
|
||||||
|
|
|
||||||
60
Sora/Tracking & Metadata/TMDB/IntroDB-FetchIntro.swift
Normal file
60
Sora/Tracking & Metadata/TMDB/IntroDB-FetchIntro.swift
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -101,4 +101,29 @@ class TMDBFetcher {
|
||||||
}
|
}
|
||||||
return dist[a.count][b.count]
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,8 @@ struct SettingsViewPlayer: View {
|
||||||
@AppStorage("doubleTapSeekEnabled") private var doubleTapSeekEnabled: Bool = false
|
@AppStorage("doubleTapSeekEnabled") private var doubleTapSeekEnabled: Bool = false
|
||||||
@AppStorage("skipIntroOutroVisible") private var skipIntroOutroVisible: Bool = true
|
@AppStorage("skipIntroOutroVisible") private var skipIntroOutroVisible: Bool = true
|
||||||
@AppStorage("autoplayNext") private var autoplayNext: 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("videoQualityWiFi") private var wifiQuality: String = VideoQualityPreference.defaultWiFiPreference.rawValue
|
||||||
@AppStorage("videoQualityCellular") private var cellularQuality: String = VideoQualityPreference.defaultCellularPreference.rawValue
|
@AppStorage("videoQualityCellular") private var cellularQuality: String = VideoQualityPreference.defaultCellularPreference.rawValue
|
||||||
|
|
||||||
|
|
@ -408,7 +409,13 @@ struct SettingsViewPlayer: View {
|
||||||
SettingsToggleRow(
|
SettingsToggleRow(
|
||||||
icon: "forward.frame",
|
icon: "forward.frame",
|
||||||
title: NSLocalizedString("Show Skip Intro / Outro Buttons", comment: ""),
|
title: NSLocalizedString("Show Skip Intro / Outro Buttons", comment: ""),
|
||||||
isOn: $skipIntroOutroVisible,
|
isOn: $skipIntroOutroVisible
|
||||||
|
)
|
||||||
|
|
||||||
|
SettingsToggleRow(
|
||||||
|
icon: "film",
|
||||||
|
title: NSLocalizedString("IntroDB", comment: ""),
|
||||||
|
isOn: $introDBEnabled,
|
||||||
showDivider: false
|
showDivider: false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@
|
||||||
13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */; };
|
13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */; };
|
||||||
13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD32D32D97400C1EBD7 /* Double+Extension.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 */; };
|
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 */; };
|
1E26E9E72DA9577900B9DC02 /* VolumeSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E26E9E62DA9577900B9DC02 /* VolumeSlider.swift */; };
|
||||||
1E47859B2DEBC5960095BF2F /* AnilistMatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E47859A2DEBC5960095BF2F /* AnilistMatchView.swift */; };
|
1E47859B2DEBC5960095BF2F /* AnilistMatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E47859A2DEBC5960095BF2F /* AnilistMatchView.swift */; };
|
||||||
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.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 = "<group>"; };
|
13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPlayer.swift; sourceTree = "<group>"; };
|
||||||
13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
|
13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
|
||||||
13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = "<group>"; };
|
13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = "<group>"; };
|
||||||
|
146A48DD2F02F1930017D145 /* IntroDB-FetchIntro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntroDB-FetchIntro.swift"; sourceTree = "<group>"; };
|
||||||
1E26E9E62DA9577900B9DC02 /* VolumeSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeSlider.swift; sourceTree = "<group>"; };
|
1E26E9E62DA9577900B9DC02 /* VolumeSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeSlider.swift; sourceTree = "<group>"; };
|
||||||
1E47859A2DEBC5960095BF2F /* AnilistMatchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnilistMatchView.swift; sourceTree = "<group>"; };
|
1E47859A2DEBC5960095BF2F /* AnilistMatchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnilistMatchView.swift; sourceTree = "<group>"; };
|
||||||
1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = "<group>"; };
|
1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -799,6 +801,7 @@
|
||||||
138FE1CE2DEC9FFA00936D81 /* TMDB */ = {
|
138FE1CE2DEC9FFA00936D81 /* TMDB */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
146A48DD2F02F1930017D145 /* IntroDB-FetchIntro.swift */,
|
||||||
138FE1CF2DECA00D00936D81 /* TMDB-FetchID.swift */,
|
138FE1CF2DECA00D00936D81 /* TMDB-FetchID.swift */,
|
||||||
);
|
);
|
||||||
path = TMDB;
|
path = TMDB;
|
||||||
|
|
@ -1150,6 +1153,7 @@
|
||||||
13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */,
|
13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */,
|
||||||
13637B8A2DE0EA1100BDA2FC /* UserDefaults.swift in Sources */,
|
13637B8A2DE0EA1100BDA2FC /* UserDefaults.swift in Sources */,
|
||||||
7260B66D2E32A8CB00365CDA /* OrphanedDownloadsView.swift in Sources */,
|
7260B66D2E32A8CB00365CDA /* OrphanedDownloadsView.swift in Sources */,
|
||||||
|
146A48DE2F02F1980017D145 /* IntroDB-FetchIntro.swift in Sources */,
|
||||||
0457C59D2DE78267000AFBD9 /* BookmarkGridView.swift in Sources */,
|
0457C59D2DE78267000AFBD9 /* BookmarkGridView.swift in Sources */,
|
||||||
0457C59E2DE78267000AFBD9 /* BookmarkLink.swift in Sources */,
|
0457C59E2DE78267000AFBD9 /* BookmarkLink.swift in Sources */,
|
||||||
0457C59F2DE78267000AFBD9 /* BookmarkGridItemView.swift in Sources */,
|
0457C59F2DE78267000AFBD9 /* BookmarkGridItemView.swift in Sources */,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue