react-native-nitro-player 0.7.0 โ 0.7.1-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -46
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +9 -13
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +45 -90
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +48 -182
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +21 -77
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +55 -104
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +113 -123
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +82 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +48 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +62 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +153 -1887
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +122 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +44 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +162 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +179 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +170 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +28 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +121 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +98 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +27 -18
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +150 -135
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +13 -30
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +102 -162
- package/ios/HybridDownloadManager.swift +32 -26
- package/ios/HybridEqualizer.swift +48 -35
- package/ios/HybridTrackPlayer.swift +127 -102
- package/ios/core/ListenerRegistry.swift +60 -0
- package/ios/core/TrackPlayerCore.swift +130 -2356
- package/ios/core/TrackPlayerListener.swift +395 -0
- package/ios/core/TrackPlayerNotify.swift +52 -0
- package/ios/core/TrackPlayerPlayback.swift +274 -0
- package/ios/core/TrackPlayerQueue.swift +221 -0
- package/ios/core/TrackPlayerQueueBuild.swift +493 -0
- package/ios/core/TrackPlayerTempQueue.swift +167 -0
- package/ios/core/TrackPlayerUrlLoader.swift +169 -0
- package/ios/equalizer/EqualizerCore.swift +63 -123
- package/ios/media/MediaSessionManager.swift +32 -49
- package/ios/playlist/PlaylistManager.swift +2 -9
- package/ios/queue/HybridPlayerQueue.swift +69 -66
- package/lib/hooks/useDownloadedTracks.js +16 -13
- package/lib/hooks/useEqualizer.d.ts +4 -4
- package/lib/hooks/useEqualizer.js +22 -17
- package/lib/hooks/useEqualizerPresets.d.ts +3 -3
- package/lib/hooks/useEqualizerPresets.js +12 -18
- package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +2 -2
- package/lib/specs/AudioDevices.nitro.d.ts +2 -2
- package/lib/specs/DownloadManager.nitro.d.ts +10 -10
- package/lib/specs/Equalizer.nitro.d.ts +10 -10
- package/lib/specs/TrackPlayer.nitro.d.ts +38 -16
- package/lib/types/EqualizerTypes.d.ts +3 -3
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__std__vector_TrackItem_.hpp +122 -0
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +31 -6
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +2 -2
- package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.cpp +16 -3
- package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.hpp +1 -1
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +154 -44
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +10 -10
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +130 -34
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +9 -9
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +115 -24
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +8 -8
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +243 -24
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +16 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_TrackItem__std__vector_TrackItem_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrarySpec.kt +3 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAudioDevicesSpec.kt +2 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +10 -10
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +10 -9
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +9 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +45 -8
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +74 -18
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +380 -151
- package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +10 -10
- package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +12 -9
- package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +23 -8
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +82 -8
- package/nitrogen/generated/ios/swift/Func_void_EqualizerState.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedPlaylist_.swift +58 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedTrack_.swift +58 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__std__string_.swift +58 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedPlaylist_.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedTrack_.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__std__vector_TrackItem_.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +10 -10
- package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +141 -71
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +9 -9
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +105 -41
- package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec.swift +8 -8
- package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec_cxx.swift +95 -32
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +16 -8
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +267 -32
- package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.hpp +3 -2
- package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.hpp +2 -1
- package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +10 -10
- package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +10 -9
- package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.hpp +9 -8
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +8 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +16 -8
- package/package.json +5 -5
- package/src/hooks/useDownloadedTracks.ts +17 -13
- package/src/hooks/useEqualizer.ts +26 -21
- package/src/hooks/useEqualizerPresets.ts +15 -21
- package/src/specs/AndroidAutoMediaLibrary.nitro.ts +2 -2
- package/src/specs/AudioDevices.nitro.ts +2 -2
- package/src/specs/DownloadManager.nitro.ts +10 -10
- package/src/specs/Equalizer.nitro.ts +10 -10
- package/src/specs/TrackPlayer.nitro.ts +52 -16
- package/src/types/EqualizerTypes.ts +17 -13
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
//
|
|
2
|
+
// TrackPlayerQueueBuild.swift
|
|
3
|
+
// NitroPlayer
|
|
4
|
+
//
|
|
5
|
+
// Created by Ritesh Shukla on 25/03/26.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import AVFoundation
|
|
9
|
+
|
|
10
|
+
import Foundation
|
|
11
|
+
|
|
12
|
+
extension TrackPlayerCore {
|
|
13
|
+
|
|
14
|
+
func updatePlayerQueue(tracks: [TrackItem]) {
|
|
15
|
+
NitroPlayerLogger.log("TrackPlayerCore", "\n" + String(repeating: "=", count: Constants.separatorLineLength))
|
|
16
|
+
NitroPlayerLogger.log("TrackPlayerCore", "๐ UPDATE PLAYER QUEUE - Received \(tracks.count) tracks")
|
|
17
|
+
NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "=", count: Constants.separatorLineLength))
|
|
18
|
+
|
|
19
|
+
#if DEBUG
|
|
20
|
+
for (index, track) in tracks.enumerated() {
|
|
21
|
+
let isDownloaded = DownloadManagerCore.shared.isTrackDownloaded(trackId: track.id)
|
|
22
|
+
let downloadStatus = isDownloaded ? "๐ฅ DOWNLOADED" : "๐ REMOTE"
|
|
23
|
+
NitroPlayerLogger.log("TrackPlayerCore", " [\(index + 1)] ๐ต \(track.title) - \(track.artist) (ID: \(track.id)) - \(downloadStatus)")
|
|
24
|
+
if isDownloaded {
|
|
25
|
+
if let localPath = DownloadManagerCore.shared.getLocalPath(trackId: track.id) {
|
|
26
|
+
NitroPlayerLogger.log("TrackPlayerCore", " Local path: \(localPath)")
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "=", count: Constants.separatorLineLength) + "\n")
|
|
31
|
+
#endif
|
|
32
|
+
|
|
33
|
+
// Store tracks for index tracking
|
|
34
|
+
currentTracks = tracks
|
|
35
|
+
currentTrackIndex = 0
|
|
36
|
+
NitroPlayerLogger.log("TrackPlayerCore", "๐ข Reset currentTrackIndex to 0 (will be updated by KVO observer)")
|
|
37
|
+
|
|
38
|
+
// Remove old boundary observer if exists
|
|
39
|
+
if let boundaryObserver = boundaryTimeObserver, let currentPlayer = player {
|
|
40
|
+
currentPlayer.removeTimeObserver(boundaryObserver)
|
|
41
|
+
boundaryTimeObserver = nil
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Re-enable stall waiting for the new first track
|
|
45
|
+
player?.automaticallyWaitsToMinimizeStalling = true
|
|
46
|
+
|
|
47
|
+
// Clear old preloaded assets when loading new queue
|
|
48
|
+
preloadedAssets.removeAll()
|
|
49
|
+
|
|
50
|
+
guard let existingPlayer = self.player else {
|
|
51
|
+
NitroPlayerLogger.log("TrackPlayerCore", "โ No player available")
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
NitroPlayerLogger.log("TrackPlayerCore", "๐ Removing \(existingPlayer.items().count) old items from player")
|
|
56
|
+
existingPlayer.removeAllItems()
|
|
57
|
+
|
|
58
|
+
// Lazy-load mode: if any track has no URL AND is not downloaded locally,
|
|
59
|
+
// we can't create an AVPlayerItem for it and the queue order would be wrong.
|
|
60
|
+
let isLazyLoad = tracks.contains {
|
|
61
|
+
$0.url.isEmpty && !DownloadManagerCore.shared.isTrackDownloaded(trackId: $0.id)
|
|
62
|
+
}
|
|
63
|
+
if isLazyLoad {
|
|
64
|
+
NitroPlayerLogger.log("TrackPlayerCore", "โณ Lazy-load mode โ player cleared, awaiting URL resolution")
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let items = tracks.enumerated().compactMap { (index, track) -> AVPlayerItem? in
|
|
69
|
+
let isPreload = index < Constants.gaplessPreloadCount
|
|
70
|
+
return createGaplessPlayerItem(for: track, isPreload: isPreload)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
NitroPlayerLogger.log("TrackPlayerCore", "๐ต Created \(items.count) gapless-optimized player items")
|
|
74
|
+
|
|
75
|
+
guard !items.isEmpty else {
|
|
76
|
+
NitroPlayerLogger.log("TrackPlayerCore", "โ No valid items to play")
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
NitroPlayerLogger.log("TrackPlayerCore", "๐ Adding \(items.count) new items to player")
|
|
81
|
+
|
|
82
|
+
var lastItem: AVPlayerItem? = nil
|
|
83
|
+
for (index, item) in items.enumerated() {
|
|
84
|
+
existingPlayer.insert(item, after: lastItem)
|
|
85
|
+
lastItem = item
|
|
86
|
+
|
|
87
|
+
#if DEBUG
|
|
88
|
+
if let trackId = item.trackId, let track = tracks.first(where: { $0.id == trackId }) {
|
|
89
|
+
NitroPlayerLogger.log("TrackPlayerCore", " โ Added to player queue [\(index + 1)]: \(track.title)")
|
|
90
|
+
}
|
|
91
|
+
#endif
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#if DEBUG
|
|
95
|
+
let trackById = Dictionary(uniqueKeysWithValues: tracks.map { ($0.id, $0) })
|
|
96
|
+
NitroPlayerLogger.log("TrackPlayerCore", "\n๐ VERIFICATION - Player now has \(existingPlayer.items().count) items:")
|
|
97
|
+
for (index, item) in existingPlayer.items().enumerated() {
|
|
98
|
+
if let trackId = item.trackId, let track = trackById[trackId] {
|
|
99
|
+
NitroPlayerLogger.log("TrackPlayerCore", " [\(index + 1)] โ \(track.title) - \(track.artist) (ID: \(track.id))")
|
|
100
|
+
} else {
|
|
101
|
+
NitroPlayerLogger.log("TrackPlayerCore", " [\(index + 1)] โ ๏ธ Unknown item (no trackId)")
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if let currentItem = existingPlayer.currentItem,
|
|
105
|
+
let trackId = currentItem.trackId,
|
|
106
|
+
let track = trackById[trackId]
|
|
107
|
+
{
|
|
108
|
+
NitroPlayerLogger.log("TrackPlayerCore", "โถ๏ธ Current item: \(track.title)")
|
|
109
|
+
}
|
|
110
|
+
NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "=", count: Constants.separatorLineLength) + "\n")
|
|
111
|
+
#endif
|
|
112
|
+
|
|
113
|
+
// Notify track change
|
|
114
|
+
if let firstTrack = tracks.first {
|
|
115
|
+
NitroPlayerLogger.log("TrackPlayerCore", "๐ต Emitting track change: \(firstTrack.title)")
|
|
116
|
+
notifyTrackChange(firstTrack, nil)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Start preloading upcoming tracks for gapless playback
|
|
120
|
+
preloadUpcomingTracks(from: 1)
|
|
121
|
+
|
|
122
|
+
NitroPlayerLogger.log("TrackPlayerCore", "โ
Queue updated with \(items.count) gapless-optimized tracks")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Clears temporary tracks, rebuilds AVQueuePlayer from `index` in the original playlist,
|
|
126
|
+
/// and resumes playback only if the player was already playing (preserves paused state).
|
|
127
|
+
@discardableResult
|
|
128
|
+
func rebuildQueueFromPlaylistIndex(index: Int) -> Bool {
|
|
129
|
+
guard index >= 0 && index < self.currentTracks.count else {
|
|
130
|
+
NitroPlayerLogger.log("TrackPlayerCore", "โ rebuildQueueFromPlaylistIndex - invalid index \(index), currentTracks.count = \(self.currentTracks.count)")
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
NitroPlayerLogger.log("TrackPlayerCore", "\n๐ฏ REBUILD QUEUE FROM PLAYLIST INDEX \(index)")
|
|
135
|
+
NitroPlayerLogger.log("TrackPlayerCore", " Total tracks in playlist: \(self.currentTracks.count)")
|
|
136
|
+
NitroPlayerLogger.log("TrackPlayerCore", " Current index: \(self.currentTrackIndex), target index: \(index)")
|
|
137
|
+
|
|
138
|
+
// Preserve playback state โ only resume if already playing.
|
|
139
|
+
let wasPlaying = self.player?.rate ?? 0 > 0
|
|
140
|
+
|
|
141
|
+
// Clear temporary tracks when jumping to specific index
|
|
142
|
+
self.playNextStack.removeAll()
|
|
143
|
+
self.upNextQueue.removeAll()
|
|
144
|
+
self.currentTemporaryType = .none
|
|
145
|
+
NitroPlayerLogger.log("TrackPlayerCore", " ๐งน Cleared temporary tracks")
|
|
146
|
+
|
|
147
|
+
let fullPlaylist = self.currentTracks
|
|
148
|
+
|
|
149
|
+
// Update currentTrackIndex BEFORE updating queue
|
|
150
|
+
self.currentTrackIndex = index
|
|
151
|
+
|
|
152
|
+
// Lazy-load guard: if the target track has no URL AND is not downloaded locally,
|
|
153
|
+
// the queue can't be built. Defer to updateTracks once URL resolution completes.
|
|
154
|
+
let targetTrack = fullPlaylist[index]
|
|
155
|
+
let isLazyLoad = targetTrack.url.isEmpty
|
|
156
|
+
&& !DownloadManagerCore.shared.isTrackDownloaded(trackId: targetTrack.id)
|
|
157
|
+
if isLazyLoad {
|
|
158
|
+
NitroPlayerLogger.log("TrackPlayerCore", " โณ Lazy-load โ deferring AVQueuePlayer setup; emitting track change for index \(index)")
|
|
159
|
+
self.currentTracks = fullPlaylist
|
|
160
|
+
if let track = self.currentTracks[safe: index] {
|
|
161
|
+
notifyTrackChange(track, .skip)
|
|
162
|
+
}
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let tracksToPlay = Array(fullPlaylist[index...])
|
|
167
|
+
NitroPlayerLogger.log("TrackPlayerCore", " ๐ Creating gapless queue with \(tracksToPlay.count) tracks starting from index \(index)")
|
|
168
|
+
|
|
169
|
+
let items = tracksToPlay.enumerated().compactMap { (offset, track) -> AVPlayerItem? in
|
|
170
|
+
let isPreload = offset < Constants.gaplessPreloadCount
|
|
171
|
+
return self.createGaplessPlayerItem(for: track, isPreload: isPreload)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
guard let player = self.player, !items.isEmpty else {
|
|
175
|
+
NitroPlayerLogger.log("TrackPlayerCore", "โ No player or no items to play")
|
|
176
|
+
return false
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Remove old boundary observer
|
|
180
|
+
if let boundaryObserver = self.boundaryTimeObserver {
|
|
181
|
+
player.removeTimeObserver(boundaryObserver)
|
|
182
|
+
self.boundaryTimeObserver = nil
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Re-enable stall waiting for the new first track
|
|
186
|
+
player.automaticallyWaitsToMinimizeStalling = true
|
|
187
|
+
|
|
188
|
+
player.removeAllItems()
|
|
189
|
+
var lastItem: AVPlayerItem? = nil
|
|
190
|
+
for item in items {
|
|
191
|
+
player.insert(item, after: lastItem)
|
|
192
|
+
lastItem = item
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Restore the full playlist reference (don't slice it!)
|
|
196
|
+
self.currentTracks = fullPlaylist
|
|
197
|
+
|
|
198
|
+
NitroPlayerLogger.log("TrackPlayerCore", " โ
Gapless queue recreated. Now at index: \(self.currentTrackIndex)")
|
|
199
|
+
if let track = self.getCurrentTrack() {
|
|
200
|
+
NitroPlayerLogger.log("TrackPlayerCore", " ๐ต Playing: \(track.title)")
|
|
201
|
+
notifyTrackChange(track, .skip)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
self.preloadUpcomingTracks(from: index + 1)
|
|
205
|
+
|
|
206
|
+
if wasPlaying { player.play() }
|
|
207
|
+
return true
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Rebuilds the AVQueuePlayer from the current position with temporary tracks.
|
|
211
|
+
/// Order: [current] + [playNext stack] + [upNext queue] + [remaining original]
|
|
212
|
+
///
|
|
213
|
+
/// - Parameter changedTrackIds: When non-nil, performs a surgical update:
|
|
214
|
+
/// only AVPlayerItems whose track ID is in this set are removed and re-created.
|
|
215
|
+
func rebuildAVQueueFromCurrentPosition(changedTrackIds: Set<String>? = nil) {
|
|
216
|
+
guard let player = self.player else { return }
|
|
217
|
+
|
|
218
|
+
let currentItem = player.currentItem
|
|
219
|
+
let playingItems = player.items()
|
|
220
|
+
|
|
221
|
+
// If the currently playing AVPlayerItem is no longer in currentTracks,
|
|
222
|
+
// delegate to rebuildQueueFromPlaylistIndex so the player immediately
|
|
223
|
+
// starts what is now at currentTrackIndex in the updated list.
|
|
224
|
+
if let playingTrackId = currentItem?.trackId,
|
|
225
|
+
!currentTracks.contains(where: { $0.id == playingTrackId }) {
|
|
226
|
+
let targetIndex = currentTrackIndex < currentTracks.count
|
|
227
|
+
? currentTrackIndex : currentTracks.count - 1
|
|
228
|
+
if targetIndex >= 0 {
|
|
229
|
+
_ = rebuildQueueFromPlaylistIndex(index: targetIndex)
|
|
230
|
+
}
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Build the desired upcoming track list
|
|
235
|
+
var newQueueTracks: [TrackItem] = []
|
|
236
|
+
let currentId = currentItem?.trackId
|
|
237
|
+
|
|
238
|
+
// PlayNext stack: skip the currently playing track by ID (not position)
|
|
239
|
+
if currentTemporaryType == .playNext, let currentId = currentId {
|
|
240
|
+
var skipped = false
|
|
241
|
+
for track in playNextStack {
|
|
242
|
+
if !skipped && track.id == currentId { skipped = true; continue }
|
|
243
|
+
newQueueTracks.append(track)
|
|
244
|
+
}
|
|
245
|
+
} else if currentTemporaryType != .playNext {
|
|
246
|
+
newQueueTracks.append(contentsOf: playNextStack)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// UpNext queue: skip the currently playing track by ID (not position)
|
|
250
|
+
if currentTemporaryType == .upNext, let currentId = currentId {
|
|
251
|
+
var skipped = false
|
|
252
|
+
for track in upNextQueue {
|
|
253
|
+
if !skipped && track.id == currentId { skipped = true; continue }
|
|
254
|
+
newQueueTracks.append(track)
|
|
255
|
+
}
|
|
256
|
+
} else if currentTemporaryType != .upNext {
|
|
257
|
+
newQueueTracks.append(contentsOf: upNextQueue)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if currentTrackIndex + 1 < currentTracks.count {
|
|
261
|
+
newQueueTracks.append(contentsOf: currentTracks[(currentTrackIndex + 1)...])
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Collect existing upcoming AVPlayerItems
|
|
265
|
+
let upcomingItems: [AVPlayerItem]
|
|
266
|
+
if let ci = currentItem, let ciIndex = playingItems.firstIndex(of: ci) {
|
|
267
|
+
upcomingItems = Array(playingItems.suffix(from: playingItems.index(after: ciIndex)))
|
|
268
|
+
} else {
|
|
269
|
+
upcomingItems = []
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let existingIds = upcomingItems.compactMap { $0.trackId }
|
|
273
|
+
let desiredIds = newQueueTracks.map { $0.id }
|
|
274
|
+
|
|
275
|
+
// Fast-path: nothing to do if queue already matches
|
|
276
|
+
if existingIds == desiredIds {
|
|
277
|
+
if let changedIds = changedTrackIds {
|
|
278
|
+
if Set(existingIds).isDisjoint(with: changedIds) {
|
|
279
|
+
NitroPlayerLogger.log("TrackPlayerCore",
|
|
280
|
+
"โ
Queue matches & no buffered URLs changed โ preserving \(existingIds.count) items for gapless")
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
NitroPlayerLogger.log("TrackPlayerCore",
|
|
285
|
+
"โ
Queue already matches desired order โ preserving \(existingIds.count) items for gapless")
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Surgical path (changedTrackIds provided, e.g. from updateTracks)
|
|
291
|
+
if let changedIds = changedTrackIds {
|
|
292
|
+
var reusableByTrackId: [String: AVPlayerItem] = [:]
|
|
293
|
+
for item in upcomingItems {
|
|
294
|
+
if let trackId = item.trackId, !changedIds.contains(trackId) {
|
|
295
|
+
reusableByTrackId[trackId] = item
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let desiredIdSet = Set(desiredIds)
|
|
300
|
+
for item in upcomingItems {
|
|
301
|
+
guard let trackId = item.trackId else { continue }
|
|
302
|
+
if changedIds.contains(trackId) || !desiredIdSet.contains(trackId) {
|
|
303
|
+
player.remove(item)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
var lastAnchor: AVPlayerItem? = currentItem
|
|
308
|
+
for (offset, trackId) in desiredIds.enumerated() {
|
|
309
|
+
if let reusable = reusableByTrackId[trackId] {
|
|
310
|
+
lastAnchor = reusable
|
|
311
|
+
} else if let track = newQueueTracks.first(where: { $0.id == trackId }),
|
|
312
|
+
let newItem = createGaplessPlayerItem(for: track, isPreload: offset < Constants.gaplessPreloadCount)
|
|
313
|
+
{
|
|
314
|
+
player.insert(newItem, after: lastAnchor)
|
|
315
|
+
lastAnchor = newItem
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
let preserved = reusableByTrackId.count
|
|
320
|
+
let inserted = desiredIds.count - preserved
|
|
321
|
+
NitroPlayerLogger.log("TrackPlayerCore",
|
|
322
|
+
"๐ Surgical rebuild: preserved \(preserved) buffered items, inserted \(inserted) new items")
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Full rebuild path (no changedTrackIds โ skip, reorder, etc.)
|
|
327
|
+
for item in playingItems where item != currentItem {
|
|
328
|
+
player.remove(item)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
var lastItem = currentItem
|
|
332
|
+
for (offset, track) in newQueueTracks.enumerated() {
|
|
333
|
+
let isPreload = offset < Constants.gaplessPreloadCount
|
|
334
|
+
if let item = createGaplessPlayerItem(for: track, isPreload: isPreload) {
|
|
335
|
+
player.insert(item, after: lastItem)
|
|
336
|
+
lastItem = item
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/// Creates a gapless-optimized AVPlayerItem with proper buffering configuration
|
|
342
|
+
func createGaplessPlayerItem(for track: TrackItem, isPreload: Bool = false) -> AVPlayerItem? {
|
|
343
|
+
let effectiveUrlString = DownloadManagerCore.shared.getEffectiveUrl(track: track)
|
|
344
|
+
|
|
345
|
+
let url: URL
|
|
346
|
+
let isLocal = effectiveUrlString.hasPrefix("/")
|
|
347
|
+
|
|
348
|
+
if isLocal {
|
|
349
|
+
NitroPlayerLogger.log("TrackPlayerCore", "๐ฅ Using DOWNLOADED version for \(track.title)")
|
|
350
|
+
NitroPlayerLogger.log("TrackPlayerCore", " Local path: \(effectiveUrlString)")
|
|
351
|
+
|
|
352
|
+
if FileManager.default.fileExists(atPath: effectiveUrlString) {
|
|
353
|
+
url = URL(fileURLWithPath: effectiveUrlString)
|
|
354
|
+
NitroPlayerLogger.log("TrackPlayerCore", " File URL: \(url.absoluteString)")
|
|
355
|
+
NitroPlayerLogger.log("TrackPlayerCore", " โ
File verified to exist")
|
|
356
|
+
} else {
|
|
357
|
+
NitroPlayerLogger.log("TrackPlayerCore", " โ Downloaded file does NOT exist at path!")
|
|
358
|
+
NitroPlayerLogger.log("TrackPlayerCore", " Falling back to remote URL: \(track.url)")
|
|
359
|
+
guard let remoteUrl = URL(string: track.url) else {
|
|
360
|
+
NitroPlayerLogger.log("TrackPlayerCore", "โ Invalid remote URL: \(track.url)")
|
|
361
|
+
return nil
|
|
362
|
+
}
|
|
363
|
+
url = remoteUrl
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
guard let remoteUrl = URL(string: effectiveUrlString) else {
|
|
367
|
+
NitroPlayerLogger.log("TrackPlayerCore", "โ Invalid URL for track: \(track.title) - \(effectiveUrlString)")
|
|
368
|
+
return nil
|
|
369
|
+
}
|
|
370
|
+
url = remoteUrl
|
|
371
|
+
NitroPlayerLogger.log("TrackPlayerCore", "๐ Using REMOTE version for \(track.title)")
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let asset: AVURLAsset
|
|
375
|
+
if let preloadedAsset = preloadedAssets[track.id] {
|
|
376
|
+
asset = preloadedAsset
|
|
377
|
+
NitroPlayerLogger.log("TrackPlayerCore", "๐ Using preloaded asset for \(track.title)")
|
|
378
|
+
} else {
|
|
379
|
+
asset = AVURLAsset(url: url, options: [
|
|
380
|
+
AVURLAssetPreferPreciseDurationAndTimingKey: true
|
|
381
|
+
])
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let item = AVPlayerItem(asset: asset)
|
|
385
|
+
|
|
386
|
+
// Let the system choose the optimal forward buffer size (0 = automatic).
|
|
387
|
+
item.preferredForwardBufferDuration = 0
|
|
388
|
+
|
|
389
|
+
item.canUseNetworkResourcesForLiveStreamingWhilePaused = true
|
|
390
|
+
item.trackId = track.id
|
|
391
|
+
|
|
392
|
+
if isPreload {
|
|
393
|
+
asset.loadValuesAsynchronously(forKeys: Constants.preloadAssetKeys) {
|
|
394
|
+
var allKeysLoaded = true
|
|
395
|
+
for key in Constants.preloadAssetKeys {
|
|
396
|
+
var error: NSError?
|
|
397
|
+
let status = asset.statusOfValue(forKey: key, error: &error)
|
|
398
|
+
if status == .failed {
|
|
399
|
+
NitroPlayerLogger.log("TrackPlayerCore", "โ ๏ธ Failed to load key '\(key)' for \(track.title): \(error?.localizedDescription ?? "unknown")")
|
|
400
|
+
allKeysLoaded = false
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if allKeysLoaded {
|
|
404
|
+
NitroPlayerLogger.log("TrackPlayerCore", "โ
All asset keys preloaded for \(track.title)")
|
|
405
|
+
}
|
|
406
|
+
// "tracks" key is now loaded โ EQ tap attaches synchronously
|
|
407
|
+
EqualizerCore.shared.applyAudioMix(to: item)
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
EqualizerCore.shared.applyAudioMix(to: item)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return item
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/// Preloads assets for upcoming tracks to enable gapless playback
|
|
417
|
+
func preloadUpcomingTracks(from startIndex: Int) {
|
|
418
|
+
// Capture the set of track IDs that already have AVPlayerItems in the queue.
|
|
419
|
+
let queuedTrackIds = Set(player?.items().compactMap { $0.trackId } ?? [])
|
|
420
|
+
|
|
421
|
+
preloadQueue.async { [weak self] in
|
|
422
|
+
guard let self else { return }
|
|
423
|
+
|
|
424
|
+
let tracks = self.currentTracks
|
|
425
|
+
let endIndex = min(startIndex + Constants.gaplessPreloadCount, tracks.count)
|
|
426
|
+
|
|
427
|
+
for i in startIndex..<endIndex {
|
|
428
|
+
guard i < tracks.count else { break }
|
|
429
|
+
let track = tracks[i]
|
|
430
|
+
|
|
431
|
+
if self.preloadedAssets[track.id] != nil || queuedTrackIds.contains(track.id) {
|
|
432
|
+
continue
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
let effectiveUrlString = DownloadManagerCore.shared.getEffectiveUrl(track: track)
|
|
436
|
+
let isLocal = effectiveUrlString.hasPrefix("/")
|
|
437
|
+
|
|
438
|
+
let url: URL
|
|
439
|
+
if isLocal {
|
|
440
|
+
url = URL(fileURLWithPath: effectiveUrlString)
|
|
441
|
+
} else {
|
|
442
|
+
guard let remoteUrl = URL(string: effectiveUrlString) else { continue }
|
|
443
|
+
url = remoteUrl
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let asset = AVURLAsset(url: url, options: [
|
|
447
|
+
AVURLAssetPreferPreciseDurationAndTimingKey: true
|
|
448
|
+
])
|
|
449
|
+
|
|
450
|
+
asset.loadValuesAsynchronously(forKeys: Constants.preloadAssetKeys) { [weak self] in
|
|
451
|
+
var allKeysLoaded = true
|
|
452
|
+
for key in Constants.preloadAssetKeys {
|
|
453
|
+
var error: NSError?
|
|
454
|
+
let status = asset.statusOfValue(forKey: key, error: &error)
|
|
455
|
+
if status != .loaded {
|
|
456
|
+
allKeysLoaded = false
|
|
457
|
+
break
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if allKeysLoaded {
|
|
462
|
+
self?.playerQueue.async {
|
|
463
|
+
self?.preloadedAssets[track.id] = asset
|
|
464
|
+
NitroPlayerLogger.log("TrackPlayerCore", "๐ฏ Preloaded asset for upcoming track: \(track.title)")
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/// Clears preloaded assets that are no longer needed
|
|
473
|
+
func cleanupPreloadedAssets(keepingFrom currentIndex: Int) {
|
|
474
|
+
// Already on playerQueue โ access preloadedAssets directly
|
|
475
|
+
let keepRange =
|
|
476
|
+
currentIndex..<min(
|
|
477
|
+
currentIndex + Constants.gaplessPreloadCount + 1, self.currentTracks.count)
|
|
478
|
+
let keepIds = Set(keepRange.compactMap { self.currentTracks[safe: $0]?.id })
|
|
479
|
+
|
|
480
|
+
let assetsToRemove = self.preloadedAssets.keys.filter { !keepIds.contains($0) }
|
|
481
|
+
for id in assetsToRemove {
|
|
482
|
+
self.preloadedAssets.removeValue(forKey: id)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if !assetsToRemove.isEmpty {
|
|
486
|
+
NitroPlayerLogger.log("TrackPlayerCore", "๐งน Cleaned up \(assetsToRemove.count) preloaded assets")
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
func getAllPlaylists() -> [Playlist] {
|
|
491
|
+
playlistManager.getAllPlaylists().map { $0.toGeneratedPlaylist() }
|
|
492
|
+
}
|
|
493
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
//
|
|
2
|
+
// TrackPlayerTempQueue.swift
|
|
3
|
+
// NitroPlayer
|
|
4
|
+
//
|
|
5
|
+
// Created by Ritesh Shukla on 25/03/26.
|
|
6
|
+
//
|
|
7
|
+
import Foundation
|
|
8
|
+
|
|
9
|
+
extension TrackPlayerCore {
|
|
10
|
+
|
|
11
|
+
func loadPlaylist(playlistId: String) async {
|
|
12
|
+
await withPlayerQueueNoThrow {
|
|
13
|
+
self.playNextStack.removeAll()
|
|
14
|
+
self.upNextQueue.removeAll()
|
|
15
|
+
self.currentTemporaryType = .none
|
|
16
|
+
|
|
17
|
+
NitroPlayerLogger.log("TrackPlayerCore", "\n" + String(repeating: "๐ผ", count: Constants.playlistSeparatorLength))
|
|
18
|
+
NitroPlayerLogger.log("TrackPlayerCore", "๐ LOAD PLAYLIST REQUEST")
|
|
19
|
+
NitroPlayerLogger.log("TrackPlayerCore", " Playlist ID: \(playlistId)")
|
|
20
|
+
NitroPlayerLogger.log("TrackPlayerCore", " ๐งน Cleared temporary tracks")
|
|
21
|
+
|
|
22
|
+
guard let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) else {
|
|
23
|
+
NitroPlayerLogger.log("TrackPlayerCore", " โ Playlist NOT FOUND")
|
|
24
|
+
NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "๐ผ", count: Constants.playlistSeparatorLength) + "\n")
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
NitroPlayerLogger.log("TrackPlayerCore", " โ
Found playlist: \(playlist.name)")
|
|
29
|
+
NitroPlayerLogger.log("TrackPlayerCore", " ๐ Contains \(playlist.tracks.count) tracks:")
|
|
30
|
+
for (index, track) in playlist.tracks.enumerated() {
|
|
31
|
+
NitroPlayerLogger.log("TrackPlayerCore", " [\(index + 1)] \(track.title) - \(track.artist)")
|
|
32
|
+
}
|
|
33
|
+
NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "๐ผ", count: Constants.playlistSeparatorLength) + "\n")
|
|
34
|
+
|
|
35
|
+
self.currentPlaylistId = playlistId
|
|
36
|
+
self.updatePlayerQueue(tracks: playlist.tracks)
|
|
37
|
+
self.emitStateChange()
|
|
38
|
+
self.checkUpcomingTracksForUrls(lookahead: self.lookaheadCount)
|
|
39
|
+
self.notifyTemporaryQueueChange()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func updatePlaylist(playlistId: String) {
|
|
44
|
+
guard currentPlaylistId == playlistId else { return }
|
|
45
|
+
|
|
46
|
+
// Cancel any pending rebuild so back-to-back calls collapse into a single rebuild.
|
|
47
|
+
pendingPlaylistUpdateWorkItem?.cancel()
|
|
48
|
+
|
|
49
|
+
let workItem = DispatchWorkItem { [weak self] in
|
|
50
|
+
guard let self, self.currentPlaylistId == playlistId,
|
|
51
|
+
let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) else { return }
|
|
52
|
+
|
|
53
|
+
// If nothing is playing yet, do a full load
|
|
54
|
+
guard self.player?.currentItem != nil else {
|
|
55
|
+
self.updatePlayerQueue(tracks: playlist.tracks)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Update tracks list without interrupting playback
|
|
60
|
+
self.currentTracks = playlist.tracks
|
|
61
|
+
self.rebuildAVQueueFromCurrentPosition()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pendingPlaylistUpdateWorkItem = workItem
|
|
65
|
+
playerQueue.async(execute: workItem)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func playNext(trackId: String) async throws {
|
|
69
|
+
try await withPlayerQueue {
|
|
70
|
+
guard let track = self.findTrackById(trackId) else {
|
|
71
|
+
throw NSError(domain: "NitroPlayer", code: 404, userInfo: [NSLocalizedDescriptionKey: "Track \(trackId) not found"])
|
|
72
|
+
}
|
|
73
|
+
NitroPlayerLogger.log("TrackPlayerCore", "โญ๏ธ playNext(\(trackId))")
|
|
74
|
+
self.playNextStack.insert(track, at: 0)
|
|
75
|
+
NitroPlayerLogger.log("TrackPlayerCore", " โ
Added '\(track.title)' to playNext stack (position: 1)")
|
|
76
|
+
if self.player?.currentItem != nil { self.rebuildAVQueueFromCurrentPosition() }
|
|
77
|
+
self.notifyTemporaryQueueChange()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func addToUpNext(trackId: String) async throws {
|
|
82
|
+
try await withPlayerQueue {
|
|
83
|
+
guard let track = self.findTrackById(trackId) else {
|
|
84
|
+
throw NSError(domain: "NitroPlayer", code: 404, userInfo: [NSLocalizedDescriptionKey: "Track \(trackId) not found"])
|
|
85
|
+
}
|
|
86
|
+
NitroPlayerLogger.log("TrackPlayerCore", "๐ addToUpNext(\(trackId))")
|
|
87
|
+
self.upNextQueue.append(track)
|
|
88
|
+
NitroPlayerLogger.log("TrackPlayerCore", " โ
Added '\(track.title)' to upNext queue (position: \(self.upNextQueue.count))")
|
|
89
|
+
if self.player?.currentItem != nil { self.rebuildAVQueueFromCurrentPosition() }
|
|
90
|
+
self.notifyTemporaryQueueChange()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func removeFromPlayNext(trackId: String) async -> Bool {
|
|
95
|
+
await withPlayerQueueNoThrow {
|
|
96
|
+
guard let idx = self.playNextStack.firstIndex(where: { $0.id == trackId }) else { return false }
|
|
97
|
+
self.playNextStack.remove(at: idx)
|
|
98
|
+
if self.player?.currentItem != nil { self.rebuildAVQueueFromCurrentPosition() }
|
|
99
|
+
self.notifyTemporaryQueueChange()
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func removeFromUpNext(trackId: String) async -> Bool {
|
|
105
|
+
await withPlayerQueueNoThrow {
|
|
106
|
+
guard let idx = self.upNextQueue.firstIndex(where: { $0.id == trackId }) else { return false }
|
|
107
|
+
self.upNextQueue.remove(at: idx)
|
|
108
|
+
if self.player?.currentItem != nil { self.rebuildAVQueueFromCurrentPosition() }
|
|
109
|
+
self.notifyTemporaryQueueChange()
|
|
110
|
+
return true
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func clearPlayNext() async {
|
|
115
|
+
await withPlayerQueueNoThrow {
|
|
116
|
+
self.playNextStack.removeAll()
|
|
117
|
+
if self.player?.currentItem != nil { self.rebuildAVQueueFromCurrentPosition() }
|
|
118
|
+
self.notifyTemporaryQueueChange()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func clearUpNext() async {
|
|
123
|
+
await withPlayerQueueNoThrow {
|
|
124
|
+
self.upNextQueue.removeAll()
|
|
125
|
+
if self.player?.currentItem != nil { self.rebuildAVQueueFromCurrentPosition() }
|
|
126
|
+
self.notifyTemporaryQueueChange()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func reorderTemporaryTrack(trackId: String, newIndex: Int) async -> Bool {
|
|
131
|
+
await withPlayerQueueNoThrow {
|
|
132
|
+
var combined = self.playNextStack + self.upNextQueue
|
|
133
|
+
guard let fromIdx = combined.firstIndex(where: { $0.id == trackId }) else { return false }
|
|
134
|
+
let track = combined.remove(at: fromIdx)
|
|
135
|
+
let clamped = newIndex.clamped(to: 0...combined.count)
|
|
136
|
+
combined.insert(track, at: clamped)
|
|
137
|
+
let pnSize = self.playNextStack.count
|
|
138
|
+
self.playNextStack = Array(combined.prefix(pnSize))
|
|
139
|
+
self.upNextQueue = Array(combined.dropFirst(pnSize))
|
|
140
|
+
if self.player?.currentItem != nil { self.rebuildAVQueueFromCurrentPosition() }
|
|
141
|
+
self.notifyTemporaryQueueChange()
|
|
142
|
+
return true
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
func getPlayNextQueue() async -> [TrackItem] {
|
|
147
|
+
await withPlayerQueueNoThrow { self.playNextStack }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
func getUpNextQueue() async -> [TrackItem] {
|
|
151
|
+
await withPlayerQueueNoThrow { self.upNextQueue }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
func findTrackById(_ trackId: String) -> TrackItem? {
|
|
155
|
+
if let t = currentTracks.first(where: { $0.id == trackId }) { return t }
|
|
156
|
+
for playlist in playlistManager.getAllPlaylists() {
|
|
157
|
+
if let t = playlist.tracks.first(where: { $0.id == trackId }) { return t }
|
|
158
|
+
}
|
|
159
|
+
return nil
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private extension Comparable {
|
|
164
|
+
func clamped(to range: ClosedRange<Self>) -> Self {
|
|
165
|
+
min(max(self, range.lowerBound), range.upperBound)
|
|
166
|
+
}
|
|
167
|
+
}
|