add IntroDB (#261)
Some checks failed
Build and Release / Build IPA (push) Has been cancelled
Build and Release / Build macOS App (push) Has been cancelled

This commit is contained in:
Mousica 2026-01-02 11:22:25 +02:00 committed by GitHub
parent 97b81186ae
commit 4344ee10fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 142 additions and 3 deletions

View file

@ -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)

View 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()
}
}

View file

@ -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()
}
}

View file

@ -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
)
}

View file

@ -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 */,