mirror of
https://github.com/cranci1/Sora.git
synced 2026-01-11 20:10:24 +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 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)
|
||||
|
|
|
|||
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]
|
||||
}
|
||||
|
||||
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("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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = "<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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -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 */,
|
||||
|
|
|
|||
Loading…
Reference in a new issue