react-native-nitro-player 0.0.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/NitroPlayer.podspec +31 -0
- package/README.md +610 -0
- package/android/CMakeLists.txt +29 -0
- package/android/build.gradle +147 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +7 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +29 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +116 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +167 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +93 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/NitroPlayerPackage.kt +21 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/connection/AndroidAutoConnectionDetector.kt +171 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +639 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +352 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibrary.kt +58 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibraryManager.kt +77 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibraryParser.kt +73 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +506 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/Playlist.kt +21 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +454 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/queue/Queue.kt +94 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/queue/QueueManager.kt +143 -0
- package/ios/HybridAudioRoutePicker.swift +53 -0
- package/ios/HybridTrackPlayer.swift +100 -0
- package/ios/core/TrackPlayerCore.swift +1040 -0
- package/ios/media/MediaSessionManager.swift +230 -0
- package/ios/playlist/PlaylistManager.swift +446 -0
- package/ios/playlist/PlaylistModel.swift +49 -0
- package/ios/queue/HybridPlayerQueue.swift +95 -0
- package/ios/queue/Queue.swift +126 -0
- package/ios/queue/QueueManager.swift +157 -0
- package/lib/hooks/index.d.ts +6 -0
- package/lib/hooks/index.js +6 -0
- package/lib/hooks/useAndroidAutoConnection.d.ts +13 -0
- package/lib/hooks/useAndroidAutoConnection.js +26 -0
- package/lib/hooks/useAudioDevices.d.ts +26 -0
- package/lib/hooks/useAudioDevices.js +55 -0
- package/lib/hooks/useOnChangeTrack.d.ts +9 -0
- package/lib/hooks/useOnChangeTrack.js +17 -0
- package/lib/hooks/useOnPlaybackProgressChange.d.ts +9 -0
- package/lib/hooks/useOnPlaybackProgressChange.js +19 -0
- package/lib/hooks/useOnPlaybackStateChange.d.ts +9 -0
- package/lib/hooks/useOnPlaybackStateChange.js +17 -0
- package/lib/hooks/useOnSeek.d.ts +8 -0
- package/lib/hooks/useOnSeek.js +17 -0
- package/lib/index.d.ts +14 -0
- package/lib/index.js +24 -0
- package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +21 -0
- package/lib/specs/AndroidAutoMediaLibrary.nitro.js +1 -0
- package/lib/specs/AudioDevices.nitro.d.ts +24 -0
- package/lib/specs/AudioDevices.nitro.js +1 -0
- package/lib/specs/AudioRoutePicker.nitro.d.ts +10 -0
- package/lib/specs/AudioRoutePicker.nitro.js +1 -0
- package/lib/specs/TrackPlayer.nitro.d.ts +39 -0
- package/lib/specs/TrackPlayer.nitro.js +1 -0
- package/lib/types/AndroidAutoMediaLibrary.d.ts +44 -0
- package/lib/types/AndroidAutoMediaLibrary.js +1 -0
- package/lib/types/PlayerQueue.d.ts +32 -0
- package/lib/types/PlayerQueue.js +1 -0
- package/lib/utils/androidAutoMediaLibrary.d.ts +47 -0
- package/lib/utils/androidAutoMediaLibrary.js +62 -0
- package/nitro.json +31 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroPlayer+autolinking.cmake +91 -0
- package/nitrogen/generated/android/NitroPlayer+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +88 -0
- package/nitrogen/generated/android/NitroPlayerOnLoad.hpp +25 -0
- package/nitrogen/generated/android/c++/JFunc_void_TrackItem_std__optional_Reason_.hpp +85 -0
- package/nitrogen/generated/android/c++/JFunc_void_TrackPlayerState_std__optional_Reason_.hpp +80 -0
- package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_double_double_std__optional_bool_.hpp +76 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string_Playlist_std__optional_QueueOperation_.hpp +88 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Playlist__std__optional_QueueOperation_.hpp +106 -0
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +55 -0
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +66 -0
- package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.cpp +70 -0
- package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.hpp +66 -0
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +143 -0
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +77 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +137 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +78 -0
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +65 -0
- package/nitrogen/generated/android/c++/JPlayerState.hpp +87 -0
- package/nitrogen/generated/android/c++/JPlaylist.hpp +99 -0
- package/nitrogen/generated/android/c++/JQueueOperation.hpp +65 -0
- package/nitrogen/generated/android/c++/JReason.hpp +65 -0
- package/nitrogen/generated/android/c++/JTAudioDevice.hpp +69 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +86 -0
- package/nitrogen/generated/android/c++/JTrackPlayerState.hpp +62 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.hpp +77 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_String.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_String.hpp +70 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.hpp +74 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_TrackItem_std__optional_Reason_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_TrackPlayerState_std__optional_Reason_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_bool.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_double_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_double_double_std__optional_bool_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__string_Playlist_std__optional_QueueOperation_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_Playlist__std__optional_QueueOperation_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrarySpec.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAudioDevicesSpec.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +116 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +134 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/NitroPlayerOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerConfig.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerState.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Playlist.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/QueueOperation.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Reason.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TAudioDevice.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackItem.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackPlayerState.kt +22 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Playlist.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_String.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_TrackItem.kt +59 -0
- package/nitrogen/generated/ios/NitroPlayer+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +123 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +531 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +80 -0
- package/nitrogen/generated/ios/NitroPlayerAutolinking.mm +49 -0
- package/nitrogen/generated/ios/NitroPlayerAutolinking.swift +55 -0
- package/nitrogen/generated/ios/c++/HybridAudioRoutePickerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridAudioRoutePickerSpecSwift.hpp +74 -0
- package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +167 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +174 -0
- package/nitrogen/generated/ios/swift/Func_void_TrackItem_std__optional_Reason_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_TrackPlayerState_std__optional_Reason_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_double_double.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_double_double_std__optional_bool_.swift +54 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string_Playlist_std__optional_QueueOperation_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_Playlist__std__optional_QueueOperation_.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridAudioRoutePickerSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridAudioRoutePickerSpec_cxx.swift +130 -0
- package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec.swift +68 -0
- package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec_cxx.swift +349 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +69 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +325 -0
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +115 -0
- package/nitrogen/generated/ios/swift/PlayerState.swift +181 -0
- package/nitrogen/generated/ios/swift/Playlist.swift +182 -0
- package/nitrogen/generated/ios/swift/QueueOperation.swift +48 -0
- package/nitrogen/generated/ios/swift/Reason.swift +48 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +147 -0
- package/nitrogen/generated/ios/swift/TrackPlayerState.swift +44 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_Playlist.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_String.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_TrackItem.swift +18 -0
- package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.hpp +63 -0
- package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.hpp +65 -0
- package/nitrogen/generated/shared/c++/HybridAudioRoutePickerSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridAudioRoutePickerSpec.hpp +62 -0
- package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.cpp +33 -0
- package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.hpp +87 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +34 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +91 -0
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +83 -0
- package/nitrogen/generated/shared/c++/PlayerState.hpp +103 -0
- package/nitrogen/generated/shared/c++/Playlist.hpp +97 -0
- package/nitrogen/generated/shared/c++/QueueOperation.hpp +84 -0
- package/nitrogen/generated/shared/c++/Reason.hpp +84 -0
- package/nitrogen/generated/shared/c++/TAudioDevice.hpp +87 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +102 -0
- package/nitrogen/generated/shared/c++/TrackPlayerState.hpp +80 -0
- package/package.json +172 -0
- package/react-native.config.js +16 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useAndroidAutoConnection.ts +30 -0
- package/src/hooks/useAudioDevices.ts +64 -0
- package/src/hooks/useOnChangeTrack.ts +24 -0
- package/src/hooks/useOnPlaybackProgressChange.ts +30 -0
- package/src/hooks/useOnPlaybackStateChange.ts +24 -0
- package/src/hooks/useOnSeek.ts +25 -0
- package/src/index.ts +47 -0
- package/src/specs/AndroidAutoMediaLibrary.nitro.ts +22 -0
- package/src/specs/AudioDevices.nitro.ts +25 -0
- package/src/specs/AudioRoutePicker.nitro.ts +9 -0
- package/src/specs/TrackPlayer.nitro.ts +81 -0
- package/src/types/AndroidAutoMediaLibrary.ts +58 -0
- package/src/types/PlayerQueue.ts +38 -0
- package/src/utils/androidAutoMediaLibrary.ts +66 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
//
|
|
2
|
+
// MediaSessionManager.swift
|
|
3
|
+
// NitroPlayer
|
|
4
|
+
//
|
|
5
|
+
// Created by Ritesh Shukla on 10/12/25.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import AVFoundation
|
|
9
|
+
import Foundation
|
|
10
|
+
import MediaPlayer
|
|
11
|
+
import NitroModules
|
|
12
|
+
import UIKit
|
|
13
|
+
|
|
14
|
+
class MediaSessionManager {
|
|
15
|
+
// MARK: - Constants
|
|
16
|
+
|
|
17
|
+
private enum Constants {
|
|
18
|
+
// Seek intervals (in seconds)
|
|
19
|
+
static let seekInterval: Double = 10.0
|
|
20
|
+
|
|
21
|
+
// Artwork size
|
|
22
|
+
static let artworkSize: CGFloat = 500.0
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// MARK: - Properties
|
|
26
|
+
|
|
27
|
+
private var trackPlayerCore: TrackPlayerCore?
|
|
28
|
+
private var artworkCache: [String: UIImage] = [:]
|
|
29
|
+
|
|
30
|
+
private var androidAutoEnabled: Bool = false
|
|
31
|
+
private var carPlayEnabled: Bool = false
|
|
32
|
+
private var showInNotification: Bool = true
|
|
33
|
+
|
|
34
|
+
init() {
|
|
35
|
+
setupRemoteCommandCenter()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func setTrackPlayerCore(_ core: TrackPlayerCore) {
|
|
39
|
+
trackPlayerCore = core
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func configure(
|
|
43
|
+
androidAutoEnabled: Bool?,
|
|
44
|
+
carPlayEnabled: Bool?,
|
|
45
|
+
showInNotification: Bool?
|
|
46
|
+
) {
|
|
47
|
+
if let androidAutoEnabled = androidAutoEnabled {
|
|
48
|
+
self.androidAutoEnabled = androidAutoEnabled
|
|
49
|
+
}
|
|
50
|
+
if let carPlayEnabled = carPlayEnabled {
|
|
51
|
+
self.carPlayEnabled = carPlayEnabled
|
|
52
|
+
// CarPlay is handled by the app's CarPlaySceneDelegate
|
|
53
|
+
// We just maintain the flag here for reference
|
|
54
|
+
}
|
|
55
|
+
if let showInNotification = showInNotification {
|
|
56
|
+
self.showInNotification = showInNotification
|
|
57
|
+
if showInNotification {
|
|
58
|
+
updateNowPlayingInfo()
|
|
59
|
+
} else {
|
|
60
|
+
clearNowPlayingInfo()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private func setupRemoteCommandCenter() {
|
|
66
|
+
let commandCenter = MPRemoteCommandCenter.shared()
|
|
67
|
+
|
|
68
|
+
// Play command
|
|
69
|
+
commandCenter.playCommand.isEnabled = true
|
|
70
|
+
commandCenter.playCommand.addTarget { [weak self] _ in
|
|
71
|
+
self?.trackPlayerCore?.play()
|
|
72
|
+
return .success
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Pause command
|
|
76
|
+
commandCenter.pauseCommand.isEnabled = true
|
|
77
|
+
commandCenter.pauseCommand.addTarget { [weak self] _ in
|
|
78
|
+
self?.trackPlayerCore?.pause()
|
|
79
|
+
return .success
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Toggle play/pause
|
|
83
|
+
commandCenter.togglePlayPauseCommand.isEnabled = true
|
|
84
|
+
commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
|
|
85
|
+
guard let self = self, let core = self.trackPlayerCore else { return .commandFailed }
|
|
86
|
+
let state = core.getState()
|
|
87
|
+
if state.currentState == .playing {
|
|
88
|
+
core.pause()
|
|
89
|
+
} else {
|
|
90
|
+
core.play()
|
|
91
|
+
}
|
|
92
|
+
return .success
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Next track command
|
|
96
|
+
commandCenter.nextTrackCommand.isEnabled = true
|
|
97
|
+
commandCenter.nextTrackCommand.addTarget { [weak self] _ in
|
|
98
|
+
self?.trackPlayerCore?.skipToNext()
|
|
99
|
+
return .success
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Previous track command
|
|
103
|
+
commandCenter.previousTrackCommand.isEnabled = true
|
|
104
|
+
commandCenter.previousTrackCommand.addTarget { [weak self] _ in
|
|
105
|
+
self?.trackPlayerCore?.skipToPrevious()
|
|
106
|
+
return .success
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Seek forward
|
|
110
|
+
commandCenter.seekForwardCommand.isEnabled = true
|
|
111
|
+
commandCenter.seekForwardCommand.addTarget { [weak self] event in
|
|
112
|
+
guard let self = self, let core = self.trackPlayerCore else { return .commandFailed }
|
|
113
|
+
let state = core.getState()
|
|
114
|
+
let newPosition = min(state.currentPosition + Constants.seekInterval, state.totalDuration)
|
|
115
|
+
core.seek(position: newPosition)
|
|
116
|
+
return .success
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Seek backward
|
|
120
|
+
commandCenter.seekBackwardCommand.isEnabled = true
|
|
121
|
+
commandCenter.seekBackwardCommand.addTarget { [weak self] event in
|
|
122
|
+
guard let self = self, let core = self.trackPlayerCore else { return .commandFailed }
|
|
123
|
+
let state = core.getState()
|
|
124
|
+
let newPosition = max(state.currentPosition - Constants.seekInterval, 0.0)
|
|
125
|
+
core.seek(position: newPosition)
|
|
126
|
+
return .success
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Change playback position
|
|
130
|
+
commandCenter.changePlaybackPositionCommand.isEnabled = true
|
|
131
|
+
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
|
|
132
|
+
guard let self = self,
|
|
133
|
+
let core = self.trackPlayerCore,
|
|
134
|
+
let event = event as? MPChangePlaybackPositionCommandEvent
|
|
135
|
+
else {
|
|
136
|
+
return .commandFailed
|
|
137
|
+
}
|
|
138
|
+
core.seek(position: event.positionTime)
|
|
139
|
+
return .success
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private func getCurrentTrack() -> TrackItem? {
|
|
144
|
+
return trackPlayerCore?.getCurrentTrack()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
func updateNowPlayingInfo() {
|
|
148
|
+
guard showInNotification else { return }
|
|
149
|
+
|
|
150
|
+
guard let track = getCurrentTrack(),
|
|
151
|
+
let core = trackPlayerCore
|
|
152
|
+
else {
|
|
153
|
+
clearNowPlayingInfo()
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let state = core.getState()
|
|
158
|
+
|
|
159
|
+
let nowPlayingInfo: [String: Any] = [
|
|
160
|
+
MPMediaItemPropertyTitle: track.title,
|
|
161
|
+
MPMediaItemPropertyArtist: track.artist,
|
|
162
|
+
MPMediaItemPropertyAlbumTitle: track.album,
|
|
163
|
+
MPNowPlayingInfoPropertyElapsedPlaybackTime: state.currentPosition,
|
|
164
|
+
MPMediaItemPropertyPlaybackDuration: state.totalDuration,
|
|
165
|
+
MPNowPlayingInfoPropertyPlaybackRate: state.currentState == .playing ? 1.0 : 0.0,
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
// Load artwork asynchronously
|
|
169
|
+
if let artwork = track.artwork, case .second(let artworkUrl) = artwork {
|
|
170
|
+
loadArtwork(url: artworkUrl) { [weak self] image in
|
|
171
|
+
if let image = image {
|
|
172
|
+
var updatedInfo = nowPlayingInfo
|
|
173
|
+
updatedInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(
|
|
174
|
+
boundsSize: CGSize(width: Constants.artworkSize, height: Constants.artworkSize),
|
|
175
|
+
requestHandler: { _ in image }
|
|
176
|
+
)
|
|
177
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = updatedInfo
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private func clearNowPlayingInfo() {
|
|
186
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private func loadArtwork(url: String, completion: @escaping (UIImage?) -> Void) {
|
|
190
|
+
// Check cache first
|
|
191
|
+
if let cached = artworkCache[url] {
|
|
192
|
+
completion(cached)
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
guard let imageUrl = URL(string: url) else {
|
|
197
|
+
completion(nil)
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Load image asynchronously
|
|
202
|
+
URLSession.shared.dataTask(with: imageUrl) { [weak self] data, _, _ in
|
|
203
|
+
guard let data = data,
|
|
204
|
+
let image = UIImage(data: data)
|
|
205
|
+
else {
|
|
206
|
+
completion(nil)
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Cache the image
|
|
211
|
+
self?.artworkCache[url] = image
|
|
212
|
+
DispatchQueue.main.async {
|
|
213
|
+
completion(image)
|
|
214
|
+
}
|
|
215
|
+
}.resume()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
func onTrackChanged() {
|
|
219
|
+
updateNowPlayingInfo()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
func onPlaybackStateChanged() {
|
|
223
|
+
updateNowPlayingInfo()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
func release() {
|
|
227
|
+
clearNowPlayingInfo()
|
|
228
|
+
artworkCache.removeAll()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
//
|
|
2
|
+
// PlaylistManager.swift
|
|
3
|
+
// NitroPlayer
|
|
4
|
+
//
|
|
5
|
+
// Created by Ritesh Shukla on 10/12/25.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
import NitroModules
|
|
10
|
+
|
|
11
|
+
/// Manages multiple playlists using AVPlayer's native playlist functionality
|
|
12
|
+
class PlaylistManager {
|
|
13
|
+
private var playlists: [String: PlaylistModel] = [:]
|
|
14
|
+
private var listeners: [(String, ([PlaylistModel], QueueOperation?) -> Void)] = []
|
|
15
|
+
private var playlistListeners: [String: [(String, (PlaylistModel, QueueOperation?) -> Void)]] =
|
|
16
|
+
[:]
|
|
17
|
+
private var currentPlaylistId: String?
|
|
18
|
+
private let queue = DispatchQueue(label: "com.margelo.nitro.nitroplayer.playlist")
|
|
19
|
+
|
|
20
|
+
static let shared = PlaylistManager()
|
|
21
|
+
|
|
22
|
+
private init() {
|
|
23
|
+
loadPlaylistsFromUserDefaults()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a new playlist
|
|
28
|
+
*/
|
|
29
|
+
func createPlaylist(name: String, description: String? = nil, artwork: String? = nil) -> String {
|
|
30
|
+
let id = UUID().uuidString
|
|
31
|
+
let playlist = PlaylistModel(id: id, name: name, description: description, artwork: artwork)
|
|
32
|
+
|
|
33
|
+
queue.sync {
|
|
34
|
+
playlists[id] = playlist
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
savePlaylistsToUserDefaults()
|
|
38
|
+
notifyPlaylistsChanged(.add)
|
|
39
|
+
|
|
40
|
+
return id
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Delete a playlist
|
|
45
|
+
*/
|
|
46
|
+
func deletePlaylist(playlistId: String) -> Bool {
|
|
47
|
+
let removed = queue.sync {
|
|
48
|
+
return playlists.removeValue(forKey: playlistId) != nil
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if removed {
|
|
52
|
+
if currentPlaylistId == playlistId {
|
|
53
|
+
currentPlaylistId = nil
|
|
54
|
+
}
|
|
55
|
+
playlistListeners.removeValue(forKey: playlistId)
|
|
56
|
+
savePlaylistsToUserDefaults()
|
|
57
|
+
notifyPlaylistsChanged(.remove)
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Update playlist metadata
|
|
66
|
+
*/
|
|
67
|
+
func updatePlaylist(
|
|
68
|
+
playlistId: String, name: String? = nil, description: String? = nil, artwork: String? = nil
|
|
69
|
+
) -> Bool {
|
|
70
|
+
guard let playlist = queue.sync(execute: { playlists[playlistId] }) else {
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
queue.sync {
|
|
75
|
+
playlists[playlistId] = PlaylistModel(
|
|
76
|
+
id: playlist.id,
|
|
77
|
+
name: name ?? playlist.name,
|
|
78
|
+
description: description ?? playlist.description,
|
|
79
|
+
artwork: artwork ?? playlist.artwork,
|
|
80
|
+
tracks: playlist.tracks
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
savePlaylistsToUserDefaults()
|
|
85
|
+
notifyPlaylistChanged(playlistId, .update)
|
|
86
|
+
notifyPlaylistsChanged(.update)
|
|
87
|
+
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get a playlist by ID
|
|
93
|
+
*/
|
|
94
|
+
func getPlaylist(playlistId: String) -> PlaylistModel? {
|
|
95
|
+
return queue.sync {
|
|
96
|
+
return playlists[playlistId]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get all playlists
|
|
102
|
+
*/
|
|
103
|
+
func getAllPlaylists() -> [PlaylistModel] {
|
|
104
|
+
return queue.sync {
|
|
105
|
+
return Array(playlists.values)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Add a track to a playlist
|
|
111
|
+
*/
|
|
112
|
+
func addTrackToPlaylist(playlistId: String, track: TrackItem, index: Int? = nil) -> Bool {
|
|
113
|
+
guard let playlist = queue.sync(execute: { playlists[playlistId] }) else {
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
queue.sync {
|
|
118
|
+
var tracks = playlist.tracks
|
|
119
|
+
if let index = index, index >= 0 && index <= tracks.count {
|
|
120
|
+
tracks.insert(track, at: index)
|
|
121
|
+
} else {
|
|
122
|
+
tracks.append(track)
|
|
123
|
+
}
|
|
124
|
+
playlists[playlistId] = PlaylistModel(
|
|
125
|
+
id: playlist.id,
|
|
126
|
+
name: playlist.name,
|
|
127
|
+
description: playlist.description,
|
|
128
|
+
artwork: playlist.artwork,
|
|
129
|
+
tracks: tracks
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
savePlaylistsToUserDefaults()
|
|
134
|
+
notifyPlaylistChanged(playlistId, .add)
|
|
135
|
+
|
|
136
|
+
// Update TrackPlayerCore if this is the current playlist
|
|
137
|
+
if currentPlaylistId == playlistId {
|
|
138
|
+
TrackPlayerCore.shared.updatePlaylist(playlistId: playlistId)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return true
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Add multiple tracks to a playlist at once
|
|
146
|
+
*/
|
|
147
|
+
func addTracksToPlaylist(playlistId: String, tracks: [TrackItem], index: Int? = nil) -> Bool {
|
|
148
|
+
guard let playlist = queue.sync(execute: { playlists[playlistId] }) else {
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
queue.sync {
|
|
153
|
+
var currentTracks = playlist.tracks
|
|
154
|
+
if let index = index, index >= 0 && index <= currentTracks.count {
|
|
155
|
+
currentTracks.insert(contentsOf: tracks, at: index)
|
|
156
|
+
} else {
|
|
157
|
+
currentTracks.append(contentsOf: tracks)
|
|
158
|
+
}
|
|
159
|
+
playlists[playlistId] = PlaylistModel(
|
|
160
|
+
id: playlist.id,
|
|
161
|
+
name: playlist.name,
|
|
162
|
+
description: playlist.description,
|
|
163
|
+
artwork: playlist.artwork,
|
|
164
|
+
tracks: currentTracks
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
savePlaylistsToUserDefaults()
|
|
169
|
+
notifyPlaylistChanged(playlistId, .add)
|
|
170
|
+
|
|
171
|
+
// Update TrackPlayerCore if this is the current playlist
|
|
172
|
+
if currentPlaylistId == playlistId {
|
|
173
|
+
TrackPlayerCore.shared.updatePlaylist(playlistId: playlistId)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return true
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Remove a track from a playlist
|
|
181
|
+
*/
|
|
182
|
+
func removeTrackFromPlaylist(playlistId: String, trackId: String) -> Bool {
|
|
183
|
+
guard let playlist = queue.sync(execute: { playlists[playlistId] }) else {
|
|
184
|
+
return false
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let removed = queue.sync {
|
|
188
|
+
var tracks = playlist.tracks
|
|
189
|
+
let initialCount = tracks.count
|
|
190
|
+
tracks.removeAll { $0.id == trackId }
|
|
191
|
+
let wasRemoved = tracks.count < initialCount
|
|
192
|
+
|
|
193
|
+
if wasRemoved {
|
|
194
|
+
playlists[playlistId] = PlaylistModel(
|
|
195
|
+
id: playlist.id,
|
|
196
|
+
name: playlist.name,
|
|
197
|
+
description: playlist.description,
|
|
198
|
+
artwork: playlist.artwork,
|
|
199
|
+
tracks: tracks
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return wasRemoved
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if removed {
|
|
207
|
+
savePlaylistsToUserDefaults()
|
|
208
|
+
notifyPlaylistChanged(playlistId, .remove)
|
|
209
|
+
|
|
210
|
+
// Update TrackPlayerCore if this is the current playlist
|
|
211
|
+
if currentPlaylistId == playlistId {
|
|
212
|
+
TrackPlayerCore.shared.updatePlaylist(playlistId: playlistId)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return removed
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Reorder a track in a playlist
|
|
221
|
+
*/
|
|
222
|
+
func reorderTrackInPlaylist(playlistId: String, trackId: String, newIndex: Int) -> Bool {
|
|
223
|
+
guard let playlist = queue.sync(execute: { playlists[playlistId] }) else {
|
|
224
|
+
return false
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let tracks = playlist.tracks
|
|
228
|
+
guard let oldIndex = tracks.firstIndex(where: { $0.id == trackId }),
|
|
229
|
+
newIndex >= 0 && newIndex < tracks.count
|
|
230
|
+
else {
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
queue.sync {
|
|
235
|
+
var reorderedTracks = tracks
|
|
236
|
+
let track = reorderedTracks.remove(at: oldIndex)
|
|
237
|
+
reorderedTracks.insert(track, at: newIndex)
|
|
238
|
+
|
|
239
|
+
playlists[playlistId] = PlaylistModel(
|
|
240
|
+
id: playlist.id,
|
|
241
|
+
name: playlist.name,
|
|
242
|
+
description: playlist.description,
|
|
243
|
+
artwork: playlist.artwork,
|
|
244
|
+
tracks: reorderedTracks
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
savePlaylistsToUserDefaults()
|
|
249
|
+
notifyPlaylistChanged(playlistId, .update)
|
|
250
|
+
|
|
251
|
+
// Update TrackPlayerCore if this is the current playlist
|
|
252
|
+
if currentPlaylistId == playlistId {
|
|
253
|
+
TrackPlayerCore.shared.updatePlaylist(playlistId: playlistId)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return true
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Load a playlist for playback (sets it as current)
|
|
261
|
+
*/
|
|
262
|
+
func loadPlaylist(playlistId: String) -> Bool {
|
|
263
|
+
guard let playlist = queue.sync(execute: { playlists[playlistId] }) else {
|
|
264
|
+
return false
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
currentPlaylistId = playlistId
|
|
268
|
+
|
|
269
|
+
// Update TrackPlayerCore
|
|
270
|
+
TrackPlayerCore.shared.loadPlaylist(playlistId: playlistId)
|
|
271
|
+
|
|
272
|
+
return true
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get the current playlist ID
|
|
277
|
+
*/
|
|
278
|
+
func getCurrentPlaylistId() -> String? {
|
|
279
|
+
return currentPlaylistId
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get the current playlist
|
|
284
|
+
*/
|
|
285
|
+
func getCurrentPlaylist() -> PlaylistModel? {
|
|
286
|
+
return currentPlaylistId.flatMap { id in queue.sync { playlists[id] } }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Add a listener for playlist changes
|
|
291
|
+
*/
|
|
292
|
+
func addPlaylistsChangeListener(listener: @escaping ([PlaylistModel], QueueOperation?) -> Void)
|
|
293
|
+
-> () -> Void
|
|
294
|
+
{
|
|
295
|
+
let listenerId = UUID().uuidString
|
|
296
|
+
queue.sync {
|
|
297
|
+
listeners.append((listenerId, listener))
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
self.queue.sync {
|
|
302
|
+
self.listeners.removeAll { $0.0 == listenerId }
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Add a listener for a specific playlist changes
|
|
309
|
+
*/
|
|
310
|
+
func addPlaylistChangeListener(
|
|
311
|
+
playlistId: String, listener: @escaping (PlaylistModel, QueueOperation?) -> Void
|
|
312
|
+
) -> () -> Void {
|
|
313
|
+
let listenerId = UUID().uuidString
|
|
314
|
+
queue.sync {
|
|
315
|
+
if playlistListeners[playlistId] == nil {
|
|
316
|
+
playlistListeners[playlistId] = []
|
|
317
|
+
}
|
|
318
|
+
playlistListeners[playlistId]?.append((listenerId, listener))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
self.queue.sync {
|
|
323
|
+
self.playlistListeners[playlistId]?.removeAll { $0.0 == listenerId }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private func notifyPlaylistsChanged(_ operation: QueueOperation?) {
|
|
329
|
+
let allPlaylists = queue.sync {
|
|
330
|
+
return Array(playlists.values)
|
|
331
|
+
}
|
|
332
|
+
listeners.forEach { $0.1(allPlaylists, operation) }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private func notifyPlaylistChanged(_ playlistId: String, _ operation: QueueOperation?) {
|
|
336
|
+
guard let playlist = queue.sync(execute: { playlists[playlistId] }) else {
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
playlistListeners[playlistId]?.forEach { $0.1(playlist, operation) }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private func savePlaylistsToUserDefaults() {
|
|
344
|
+
// Save playlists to UserDefaults for persistence
|
|
345
|
+
// Implementation similar to Android SharedPreferences
|
|
346
|
+
do {
|
|
347
|
+
let playlistsArray = queue.sync {
|
|
348
|
+
return Array(playlists.values)
|
|
349
|
+
}
|
|
350
|
+
let playlistsData = playlistsArray.map { playlist -> [String: Any] in
|
|
351
|
+
return [
|
|
352
|
+
"id": playlist.id,
|
|
353
|
+
"name": playlist.name,
|
|
354
|
+
"description": playlist.description ?? "",
|
|
355
|
+
"artwork": playlist.artwork ?? "",
|
|
356
|
+
"tracks": playlist.tracks.map { track -> [String: Any] in
|
|
357
|
+
var trackDict: [String: Any] = [
|
|
358
|
+
"id": track.id,
|
|
359
|
+
"title": track.title,
|
|
360
|
+
"artist": track.artist,
|
|
361
|
+
"album": track.album,
|
|
362
|
+
"duration": track.duration,
|
|
363
|
+
"url": track.url,
|
|
364
|
+
]
|
|
365
|
+
// Handle artwork - unwrap Variant_NullType_String
|
|
366
|
+
if let artwork = track.artwork, case .second(let artworkUrl) = artwork {
|
|
367
|
+
trackDict["artwork"] = artworkUrl
|
|
368
|
+
} else {
|
|
369
|
+
trackDict["artwork"] = ""
|
|
370
|
+
}
|
|
371
|
+
return trackDict
|
|
372
|
+
},
|
|
373
|
+
]
|
|
374
|
+
}
|
|
375
|
+
let data = try JSONSerialization.data(withJSONObject: playlistsData, options: [])
|
|
376
|
+
UserDefaults.standard.set(data, forKey: "NitroPlayerPlaylists")
|
|
377
|
+
UserDefaults.standard.set(currentPlaylistId, forKey: "NitroPlayerCurrentPlaylistId")
|
|
378
|
+
} catch {
|
|
379
|
+
print("❌ PlaylistManager: Error saving playlists - \(error)")
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private func loadPlaylistsFromUserDefaults() {
|
|
384
|
+
guard let data = UserDefaults.standard.data(forKey: "NitroPlayerPlaylists") else {
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
do {
|
|
389
|
+
let decoder = JSONDecoder()
|
|
390
|
+
let playlistsDict = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] ?? []
|
|
391
|
+
|
|
392
|
+
queue.sync {
|
|
393
|
+
playlists.removeAll()
|
|
394
|
+
for playlistDict in playlistsDict {
|
|
395
|
+
guard let id = playlistDict["id"] as? String,
|
|
396
|
+
let name = playlistDict["name"] as? String
|
|
397
|
+
else {
|
|
398
|
+
continue
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
let description = playlistDict["description"] as? String
|
|
402
|
+
let artwork = playlistDict["artwork"] as? String
|
|
403
|
+
let tracksArray = playlistDict["tracks"] as? [[String: Any]] ?? []
|
|
404
|
+
|
|
405
|
+
let tracks = tracksArray.compactMap { trackDict -> TrackItem? in
|
|
406
|
+
guard let id = trackDict["id"] as? String,
|
|
407
|
+
let title = trackDict["title"] as? String,
|
|
408
|
+
let artist = trackDict["artist"] as? String,
|
|
409
|
+
let album = trackDict["album"] as? String,
|
|
410
|
+
let duration = trackDict["duration"] as? Double,
|
|
411
|
+
let url = trackDict["url"] as? String
|
|
412
|
+
else {
|
|
413
|
+
return nil
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
let artworkString = trackDict["artwork"] as? String
|
|
417
|
+
let artwork = artworkString.flatMap {
|
|
418
|
+
!$0.isEmpty ? Variant_NullType_String.second($0) : nil
|
|
419
|
+
}
|
|
420
|
+
return TrackItem(
|
|
421
|
+
id: id,
|
|
422
|
+
title: title,
|
|
423
|
+
artist: artist,
|
|
424
|
+
album: album,
|
|
425
|
+
duration: duration,
|
|
426
|
+
url: url,
|
|
427
|
+
artwork: artwork
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
playlists[id] = PlaylistModel(
|
|
432
|
+
id: id,
|
|
433
|
+
name: name,
|
|
434
|
+
description: description,
|
|
435
|
+
artwork: artwork,
|
|
436
|
+
tracks: tracks
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
currentPlaylistId = UserDefaults.standard.string(forKey: "NitroPlayerCurrentPlaylistId")
|
|
442
|
+
} catch {
|
|
443
|
+
print("❌ PlaylistManager: Error loading playlists - \(error)")
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//
|
|
2
|
+
// PlaylistModel.swift
|
|
3
|
+
// NitroPlayer
|
|
4
|
+
//
|
|
5
|
+
// Created by Ritesh Shukla on 10/12/25.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
import NitroModules
|
|
10
|
+
|
|
11
|
+
/// Represents a playlist containing multiple tracks
|
|
12
|
+
/// Uses AVPlayer's native playlist functionality
|
|
13
|
+
class PlaylistModel {
|
|
14
|
+
let id: String
|
|
15
|
+
let name: String
|
|
16
|
+
let description: String?
|
|
17
|
+
let artwork: String?
|
|
18
|
+
var tracks: [TrackItem]
|
|
19
|
+
|
|
20
|
+
init(
|
|
21
|
+
id: String, name: String, description: String? = nil, artwork: String? = nil,
|
|
22
|
+
tracks: [TrackItem] = []
|
|
23
|
+
) {
|
|
24
|
+
self.id = id
|
|
25
|
+
self.name = name
|
|
26
|
+
self.description = description
|
|
27
|
+
self.artwork = artwork
|
|
28
|
+
self.tracks = tracks
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func getTrackCount() -> Int {
|
|
32
|
+
return tracks.count
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func isEmpty() -> Bool {
|
|
36
|
+
return tracks.isEmpty
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Convert to generated Playlist type
|
|
40
|
+
func toGeneratedPlaylist() -> Playlist {
|
|
41
|
+
return Playlist(
|
|
42
|
+
id: self.id,
|
|
43
|
+
name: self.name,
|
|
44
|
+
description: self.description.map { Variant_NullType_String.second($0) },
|
|
45
|
+
artwork: self.artwork.map { Variant_NullType_String.second($0) },
|
|
46
|
+
tracks: self.tracks
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|