Skip to content

Commit 0feff8a

Browse files
committed
Test TheIntroDB
1 parent 20b01b2 commit 0feff8a

File tree

6 files changed

+404
-20
lines changed

6 files changed

+404
-20
lines changed

Luna.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
13E1EDF02ED9E66000087629 /* browseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13E1EDCF2ED9E65F00087629 /* browseView.swift */; };
6868
13E1EDF12ED9E66000087629 /* kanzenModuleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13E1EDD02ED9E65F00087629 /* kanzenModuleView.swift */; };
6969
13E1EDF42ED9E67000087629 /* ContentModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 13E1EDF22ED9E67000087629 /* ContentModel.xcdatamodeld */; };
70+
13EFB91F2F644F8400F9B8E4 /* IntroDBService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EFB91E2F644F8400F9B8E4 /* IntroDBService.swift */; };
7071
13F0725F2E4B2D3300EF90EB /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13F0725E2E4B2D3300EF90EB /* SoraApp.swift */; };
7172
13F072632E4B2D3700EF90EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13F072622E4B2D3700EF90EB /* Assets.xcassets */; };
7273
13F0729B2E4B2D9200EF90EB /* ServicesEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13F0726E2E4B2D9000EF90EB /* ServicesEntity.swift */; };
@@ -188,6 +189,7 @@
188189
13E1EDCF2ED9E65F00087629 /* browseView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = browseView.swift; sourceTree = "<group>"; };
189190
13E1EDD02ED9E65F00087629 /* kanzenModuleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = kanzenModuleView.swift; sourceTree = "<group>"; };
190191
13E1EDF32ED9E67000087629 /* ContentModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = ContentModel.xcdatamodel; sourceTree = "<group>"; };
192+
13EFB91E2F644F8400F9B8E4 /* IntroDBService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntroDBService.swift; sourceTree = "<group>"; };
191193
13F0725B2E4B2D3300EF90EB /* Luna.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Luna.app; sourceTree = BUILT_PRODUCTS_DIR; };
192194
13F0725E2E4B2D3300EF90EB /* SoraApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraApp.swift; sourceTree = "<group>"; };
193195
13F072622E4B2D3700EF90EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -291,6 +293,7 @@
291293
1375466F2E5F079000B29944 /* Progress */ = {
292294
isa = PBXGroup;
293295
children = (
296+
13EFB91E2F644F8400F9B8E4 /* IntroDBService.swift */,
294297
13B6F3442EA4C6BC00DECD2B /* ProgressManager.swift */,
295298
);
296299
path = Progress;
@@ -945,6 +948,7 @@
945948
13F072A82E4B2D9200EF90EB /* ContentView.swift in Sources */,
946949
13F072B72E4B2D9200EF90EB /* Logger.swift in Sources */,
947950
13E1EDD72ED9E65F00087629 /* favouriteManager.swift in Sources */,
951+
13EFB91F2F644F8400F9B8E4 /* IntroDBService.swift in Sources */,
948952
13B2E5742E7597B9009F3271 /* JSController-NetworkFetch.swift in Sources */,
949953
13F315B02EAD03F70025BD7A /* SubtitleLoader.swift in Sources */,
950954
13F072B82E4B2D9200EF90EB /* LevenshteinDistance.swift in Sources */,

Luna/Player/Elements/MusicProgressSlider.swift

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@
44
//
55
// Created by Pratik on 08/01/23.
66
//
7-
// Thanks to pratikg29 for this code inside his open source project "https://github.com/pratikg29/Custom-Slider-Control?ref=iosexample.com"
7+
// Thanks to pratikg29 for this code inside his open source project "https://github.com/pratikg29/Custom-Slider-Control"
88
// I did edit some of the code for my liking (added a buffer indicator, etc.)
99

1010
import SwiftUI
1111

12+
struct ProgressHighlight: Identifiable {
13+
let id = UUID()
14+
let start: Double
15+
let end: Double
16+
let color: Color
17+
let label: String
18+
}
19+
1220
struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
1321
@Binding var value: T
1422
let inRange: ClosedRange<T>
@@ -17,6 +25,7 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
1725
let textColor: Color
1826
let emptyColor: Color
1927
let height: CGFloat
28+
let highlights: [ProgressHighlight]
2029
let onEditingChanged: (Bool) -> Void
2130

2231
@State private var localRealProgress: T = 0
@@ -32,6 +41,7 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
3241
textColor: Color,
3342
emptyColor: Color,
3443
height: CGFloat,
44+
highlights: [ProgressHighlight] = [],
3545
onEditingChanged: @escaping (Bool) -> Void
3646
) {
3747
self._value = value
@@ -41,6 +51,7 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
4151
self.textColor = textColor
4252
self.emptyColor = emptyColor
4353
self.height = height
54+
self.highlights = highlights
4455
self.onEditingChanged = onEditingChanged
4556
}
4657

@@ -54,9 +65,33 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
5465
ZStack(alignment: .center) {
5566
Capsule()
5667
.fill(.ultraThinMaterial)
68+
69+
if !highlights.isEmpty {
70+
Canvas { context, size in
71+
let lower = Double(inRange.lowerBound)
72+
let upper = Double(inRange.upperBound)
73+
let range = max(upper - lower, 0.000001)
74+
75+
for highlight in highlights {
76+
let clampedStart = max(lower, min(highlight.start, upper))
77+
let clampedEnd = max(lower, min(highlight.end, upper))
78+
guard clampedEnd > clampedStart else { continue }
79+
80+
let startRatio = (clampedStart - lower) / range
81+
let endRatio = (clampedEnd - lower) / range
82+
83+
let x = size.width * CGFloat(startRatio)
84+
let width = size.width * CGFloat(endRatio - startRatio)
85+
let rect = CGRect(x: x, y: 0, width: width, height: size.height)
86+
context.fill(Path(rect), with: .color(highlight.color.opacity(0.7)))
87+
}
88+
}
89+
.frame(width: bounds.size.width)
90+
.mask(Capsule())
91+
}
5792
}
5893
.clipShape(Capsule())
59-
94+
6095
Capsule()
6196
.fill(isActive ? activeFillColor : fillColor)
6297
.mask({
@@ -87,7 +122,7 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
87122
}
88123
.frame(width: bounds.size.width, height: bounds.size.height, alignment: .center)
89124
.contentShape(Rectangle())
90-
#if !os(tvOS)
125+
#if !os(tvOS)
91126
.gesture(
92127
DragGesture(minimumDistance: 0, coordinateSpace: .local)
93128
.updating($isActive) { _, state, _ in
@@ -104,7 +139,7 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
104139
localTempProgress = 0
105140
}
106141
)
107-
#endif
142+
#endif
108143
.onChangeComp(of: isActive) { _, newValue in
109144
value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound)
110145
onEditingChanged(newValue)
@@ -121,7 +156,7 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
121156
}
122157
.frame(height: isActive ? height * 1.25 : height, alignment: .center)
123158
}
124-
159+
125160
private var animation: Animation {
126161
if isActive {
127162
return .spring()

Luna/Player/MPVSoftwareRenderer.swift

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -144,22 +144,24 @@ final class MPVSoftwareRenderer {
144144
throw RendererError.mpvCreationFailed
145145
}
146146
mpv = handle
147-
setOption(name: "terminal", value: "yes")
148-
setOption(name: "msg-level", value: "status")
149-
setOption(name: "keep-open", value: "yes")
150-
setOption(name: "idle", value: "yes")
151147
setOption(name: "vo", value: "libmpv")
152-
setOption(name: "hwdec", value: "videotoolbox-copy")
148+
setOption(name: "idle", value: "yes")
149+
setOption(name: "cache", value: "yes")
150+
setOption(name: "ytdl", value: "yes")
151+
setOption(name: "hr-seek", value: "yes")
152+
setOption(name: "terminal", value: "yes")
153153
setOption(name: "gpu-api", value: "metal")
154+
setOption(name: "keep-open", value: "yes")
155+
setOption(name: "subs-fallback", value: "yes")
154156
setOption(name: "gpu-context", value: "metal")
157+
setOption(name: "msg-level", value: "all=warn")
158+
setOption(name: "interpolation", value: "no")
155159
setOption(name: "demuxer-thread", value: "yes")
156-
setOption(name: "ytdl", value: "yes")
157-
setOption(name: "profile", value: "fast")
158-
setOption(name: "vd-lavc-threads", value: "8")
159-
setOption(name: "cache", value: "yes")
160160
setOption(name: "demuxer-max-bytes", value: "150M")
161-
setOption(name: "demuxer-readahead-secs", value: "20")
162-
setOption(name: "subs-fallback", value: "yes")
161+
setOption(name: "hwdec", value: "videotoolbox-copy")
162+
setOption(name: "demuxer-readahead-secs", value: "10")
163+
setOption(name: "video-sync", value: "display-resample")
164+
setOption(name: "audio-normalize-downmix", value: "yes")
163165

164166
let initStatus = mpv_initialize(handle)
165167
guard initStatus >= 0 else {

Luna/Player/PlayerViewController.swift

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,21 @@ final class PlayerViewController: UIViewController {
177177
return b
178178
}()
179179

180+
private let skipSegmentButton: UIButton = {
181+
let b = UIButton(type: .system)
182+
b.translatesAutoresizingMaskIntoConstraints = false
183+
b.setTitle("Skip", for: .normal)
184+
b.setTitleColor(.white, for: .normal)
185+
b.titleLabel?.font = .systemFont(ofSize: 14, weight: .semibold)
186+
b.backgroundColor = UIColor(white: 0.2, alpha: 0.55)
187+
b.layer.cornerRadius = 18
188+
b.layer.cornerCurve = .continuous
189+
b.contentEdgeInsets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 12)
190+
b.alpha = 0.0
191+
b.isHidden = true
192+
return b
193+
}()
194+
180195
private let progressContainer: UIView = {
181196
let v = UIView()
182197
v.translatesAutoresizingMaskIntoConstraints = false
@@ -189,6 +204,7 @@ final class PlayerViewController: UIViewController {
189204
class ProgressModel: ObservableObject {
190205
@Published var position: Double = 0
191206
@Published var duration: Double = 1
207+
@Published var highlights: [ProgressHighlight] = []
192208
}
193209
private var progressModel = ProgressModel()
194210

@@ -296,6 +312,8 @@ final class PlayerViewController: UIViewController {
296312
private var controlsHideWorkItem: DispatchWorkItem?
297313
private var controlsVisible: Bool = true
298314
private var pendingSeekTime: Double?
315+
private var introDBSegments: [IntroDBSegment] = []
316+
private var activeSkipSegmentID: String?
299317

300318
override func viewDidLoad() {
301319
super.viewDidLoad()
@@ -399,6 +417,7 @@ final class PlayerViewController: UIViewController {
399417
renderer.load(url: url, with: preset, headers: headers)
400418
if let info = mediaInfo {
401419
prepareSeekToLastPosition(for: info)
420+
fetchIntroDBSegments(for: info)
402421
}
403422

404423
if let subs = initialSubtitles, !subs.isEmpty {
@@ -466,6 +485,7 @@ final class PlayerViewController: UIViewController {
466485
videoContainer.addSubview(skipForwardButton)
467486
videoContainer.addSubview(speedIndicatorLabel)
468487
videoContainer.addSubview(subtitleButton)
488+
videoContainer.addSubview(skipSegmentButton)
469489

470490
NSLayoutConstraint.activate([
471491
videoContainer.topAnchor.constraint(equalTo: view.topAnchor),
@@ -526,7 +546,11 @@ final class PlayerViewController: UIViewController {
526546
subtitleButton.trailingAnchor.constraint(equalTo: progressContainer.trailingAnchor, constant: 0),
527547
subtitleButton.bottomAnchor.constraint(equalTo: progressContainer.topAnchor, constant: -8),
528548
subtitleButton.widthAnchor.constraint(equalToConstant: 32),
529-
subtitleButton.heightAnchor.constraint(equalToConstant: 32)
549+
subtitleButton.heightAnchor.constraint(equalToConstant: 32),
550+
551+
skipSegmentButton.trailingAnchor.constraint(equalTo: progressContainer.trailingAnchor),
552+
skipSegmentButton.bottomAnchor.constraint(equalTo: progressContainer.topAnchor, constant: -14),
553+
skipSegmentButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 36)
530554
])
531555
}
532556

@@ -536,6 +560,7 @@ final class PlayerViewController: UIViewController {
536560
pipButton.addTarget(self, action: #selector(pipTapped), for: .touchUpInside)
537561
skipBackwardButton.addTarget(self, action: #selector(skipBackwardTapped), for: .touchUpInside)
538562
skipForwardButton.addTarget(self, action: #selector(skipForwardTapped), for: .touchUpInside)
563+
skipSegmentButton.addTarget(self, action: #selector(skipSegmentTapped), for: .touchUpInside)
539564
let tap = UITapGestureRecognizer(target: self, action: #selector(containerTapped))
540565
videoContainer.addGestureRecognizer(tap)
541566
}
@@ -856,7 +881,7 @@ final class PlayerViewController: UIViewController {
856881
@ObservedObject var model: ProgressModel
857882
var onEditingChanged: (Bool) -> Void
858883
var body: some View {
859-
MusicProgressSlider(value: Binding(get: { model.position }, set: { model.position = $0 }), inRange: 0...max(model.duration, 1.0), activeFillColor: .white, fillColor: .white, textColor: .white.opacity(0.7), emptyColor: .white.opacity(0.3), height: 33, onEditingChanged: onEditingChanged)
884+
MusicProgressSlider(value: Binding(get: { model.position }, set: { model.position = $0 }), inRange: 0...max(model.duration, 1.0), activeFillColor: .white, fillColor: .white, textColor: .white.opacity(0.7), emptyColor: .white.opacity(0.3), height: 33, highlights: model.highlights, onEditingChanged: onEditingChanged)
860885
}
861886
}
862887

@@ -1016,6 +1041,13 @@ final class PlayerViewController: UIViewController {
10161041
}
10171042
}
10181043

1044+
@objc private func skipSegmentTapped() {
1045+
guard let segment = currentActiveSegment(at: cachedPosition) else { return }
1046+
guard let target = resolvedEnd(for: segment, duration: cachedDuration) else { return }
1047+
renderer.seek(to: max(0, target))
1048+
showControlsTemporarily()
1049+
}
1050+
10191051
private func showControlsTemporarily() {
10201052
controlsHideWorkItem?.cancel()
10211053
controlsVisible = true
@@ -1090,9 +1122,11 @@ final class PlayerViewController: UIViewController {
10901122
self.cachedPosition = position
10911123
if duration > 0 {
10921124
self.updateProgressHostingController()
1125+
self.updateProgressHighlights(duration: duration)
10931126
}
10941127
self.progressModel.position = position
10951128
self.progressModel.duration = max(duration, 1.0)
1129+
self.updateActiveSkipSegment(at: position, duration: duration)
10961130

10971131
if self.pipController?.isPictureInPictureActive == true {
10981132
self.pipController?.updatePlaybackState()
@@ -1121,6 +1155,73 @@ final class PlayerViewController: UIViewController {
11211155
return String(format: "%02d:%02d", m, s)
11221156
}
11231157
}
1158+
1159+
private func fetchIntroDBSegments(for mediaInfo: MediaInfo) {
1160+
IntroDBService.shared.fetchSegments(for: mediaInfo) { [weak self] result in
1161+
guard let self = self else { return }
1162+
1163+
switch result {
1164+
case .success(let segments):
1165+
DispatchQueue.main.async {
1166+
self.introDBSegments = segments
1167+
self.updateProgressHighlights(duration: self.cachedDuration)
1168+
self.updateActiveSkipSegment(at: self.cachedPosition, duration: self.cachedDuration)
1169+
}
1170+
Logger.shared.log("Loaded \(segments.count) IntroDB segments", type: "Info")
1171+
case .failure(let error):
1172+
Logger.shared.log("IntroDB request failed: \(error.localizedDescription)", type: "Warn")
1173+
}
1174+
}
1175+
}
1176+
1177+
private func updateProgressHighlights(duration: Double) {
1178+
let highlights = IntroDBService.shared.highlights(for: introDBSegments, duration: duration)
1179+
progressModel.highlights = highlights.map {
1180+
ProgressHighlight(start: $0.start, end: $0.end, color: Color($0.color), label: $0.label)
1181+
}
1182+
}
1183+
1184+
private func currentActiveSegment(at position: Double, duration: Double? = nil) -> IntroDBSegment? {
1185+
return IntroDBService.shared.activeSegment(at: position, in: introDBSegments, duration: duration ?? cachedDuration)
1186+
}
1187+
1188+
private func updateActiveSkipSegment(at position: Double, duration: Double) {
1189+
let active = currentActiveSegment(at: position, duration: duration)
1190+
let newID = active?.id
1191+
guard newID != activeSkipSegmentID else { return }
1192+
activeSkipSegmentID = newID
1193+
1194+
if let active {
1195+
showSkipButton(for: active)
1196+
} else {
1197+
hideSkipButton()
1198+
}
1199+
}
1200+
1201+
private func showSkipButton(for segment: IntroDBSegment) {
1202+
let title = "Skip \(segment.db.title)"
1203+
skipSegmentButton.setTitle(title, for: .normal)
1204+
skipSegmentButton.backgroundColor = segment.db.uiColor.withAlphaComponent(0.55)
1205+
1206+
guard skipSegmentButton.isHidden || skipSegmentButton.alpha < 1.0 else { return }
1207+
skipSegmentButton.isHidden = false
1208+
UIView.animate(withDuration: 0.2) {
1209+
self.skipSegmentButton.alpha = 1.0
1210+
}
1211+
}
1212+
1213+
private func hideSkipButton() {
1214+
guard !skipSegmentButton.isHidden else { return }
1215+
UIView.animate(withDuration: 0.2, animations: {
1216+
self.skipSegmentButton.alpha = 0.0
1217+
}, completion: { _ in
1218+
self.skipSegmentButton.isHidden = true
1219+
})
1220+
}
1221+
1222+
private func resolvedEnd(for segment: IntroDBSegment, duration: Double) -> Double? {
1223+
return segment.resolvedEnd(duration: duration)
1224+
}
11241225
}
11251226

11261227
// MARK: - MPVSoftwareRendererDelegate

0 commit comments

Comments
 (0)