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.
Files changed (191) hide show
  1. package/NitroPlayer.podspec +31 -0
  2. package/README.md +610 -0
  3. package/android/CMakeLists.txt +29 -0
  4. package/android/build.gradle +147 -0
  5. package/android/fix-prefab.gradle +51 -0
  6. package/android/gradle.properties +5 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +7 -0
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +29 -0
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +116 -0
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +167 -0
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +93 -0
  13. package/android/src/main/java/com/margelo/nitro/nitroplayer/NitroPlayerPackage.kt +21 -0
  14. package/android/src/main/java/com/margelo/nitro/nitroplayer/connection/AndroidAutoConnectionDetector.kt +171 -0
  15. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +639 -0
  16. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +352 -0
  17. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibrary.kt +58 -0
  18. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibraryManager.kt +77 -0
  19. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibraryParser.kt +73 -0
  20. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +506 -0
  21. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/Playlist.kt +21 -0
  22. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +454 -0
  23. package/android/src/main/java/com/margelo/nitro/nitroplayer/queue/Queue.kt +94 -0
  24. package/android/src/main/java/com/margelo/nitro/nitroplayer/queue/QueueManager.kt +143 -0
  25. package/ios/HybridAudioRoutePicker.swift +53 -0
  26. package/ios/HybridTrackPlayer.swift +100 -0
  27. package/ios/core/TrackPlayerCore.swift +1040 -0
  28. package/ios/media/MediaSessionManager.swift +230 -0
  29. package/ios/playlist/PlaylistManager.swift +446 -0
  30. package/ios/playlist/PlaylistModel.swift +49 -0
  31. package/ios/queue/HybridPlayerQueue.swift +95 -0
  32. package/ios/queue/Queue.swift +126 -0
  33. package/ios/queue/QueueManager.swift +157 -0
  34. package/lib/hooks/index.d.ts +6 -0
  35. package/lib/hooks/index.js +6 -0
  36. package/lib/hooks/useAndroidAutoConnection.d.ts +13 -0
  37. package/lib/hooks/useAndroidAutoConnection.js +26 -0
  38. package/lib/hooks/useAudioDevices.d.ts +26 -0
  39. package/lib/hooks/useAudioDevices.js +55 -0
  40. package/lib/hooks/useOnChangeTrack.d.ts +9 -0
  41. package/lib/hooks/useOnChangeTrack.js +17 -0
  42. package/lib/hooks/useOnPlaybackProgressChange.d.ts +9 -0
  43. package/lib/hooks/useOnPlaybackProgressChange.js +19 -0
  44. package/lib/hooks/useOnPlaybackStateChange.d.ts +9 -0
  45. package/lib/hooks/useOnPlaybackStateChange.js +17 -0
  46. package/lib/hooks/useOnSeek.d.ts +8 -0
  47. package/lib/hooks/useOnSeek.js +17 -0
  48. package/lib/index.d.ts +14 -0
  49. package/lib/index.js +24 -0
  50. package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +21 -0
  51. package/lib/specs/AndroidAutoMediaLibrary.nitro.js +1 -0
  52. package/lib/specs/AudioDevices.nitro.d.ts +24 -0
  53. package/lib/specs/AudioDevices.nitro.js +1 -0
  54. package/lib/specs/AudioRoutePicker.nitro.d.ts +10 -0
  55. package/lib/specs/AudioRoutePicker.nitro.js +1 -0
  56. package/lib/specs/TrackPlayer.nitro.d.ts +39 -0
  57. package/lib/specs/TrackPlayer.nitro.js +1 -0
  58. package/lib/types/AndroidAutoMediaLibrary.d.ts +44 -0
  59. package/lib/types/AndroidAutoMediaLibrary.js +1 -0
  60. package/lib/types/PlayerQueue.d.ts +32 -0
  61. package/lib/types/PlayerQueue.js +1 -0
  62. package/lib/utils/androidAutoMediaLibrary.d.ts +47 -0
  63. package/lib/utils/androidAutoMediaLibrary.js +62 -0
  64. package/nitro.json +31 -0
  65. package/nitrogen/generated/.gitattributes +1 -0
  66. package/nitrogen/generated/android/NitroPlayer+autolinking.cmake +91 -0
  67. package/nitrogen/generated/android/NitroPlayer+autolinking.gradle +27 -0
  68. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +88 -0
  69. package/nitrogen/generated/android/NitroPlayerOnLoad.hpp +25 -0
  70. package/nitrogen/generated/android/c++/JFunc_void_TrackItem_std__optional_Reason_.hpp +85 -0
  71. package/nitrogen/generated/android/c++/JFunc_void_TrackPlayerState_std__optional_Reason_.hpp +80 -0
  72. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
  73. package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
  74. package/nitrogen/generated/android/c++/JFunc_void_double_double_std__optional_bool_.hpp +76 -0
  75. package/nitrogen/generated/android/c++/JFunc_void_std__string_Playlist_std__optional_QueueOperation_.hpp +88 -0
  76. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Playlist__std__optional_QueueOperation_.hpp +106 -0
  77. package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +55 -0
  78. package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +66 -0
  79. package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.cpp +70 -0
  80. package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.hpp +66 -0
  81. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +143 -0
  82. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +77 -0
  83. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +137 -0
  84. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +78 -0
  85. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +65 -0
  86. package/nitrogen/generated/android/c++/JPlayerState.hpp +87 -0
  87. package/nitrogen/generated/android/c++/JPlaylist.hpp +99 -0
  88. package/nitrogen/generated/android/c++/JQueueOperation.hpp +65 -0
  89. package/nitrogen/generated/android/c++/JReason.hpp +65 -0
  90. package/nitrogen/generated/android/c++/JTAudioDevice.hpp +69 -0
  91. package/nitrogen/generated/android/c++/JTrackItem.hpp +86 -0
  92. package/nitrogen/generated/android/c++/JTrackPlayerState.hpp +62 -0
  93. package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.cpp +26 -0
  94. package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.hpp +77 -0
  95. package/nitrogen/generated/android/c++/JVariant_NullType_String.cpp +26 -0
  96. package/nitrogen/generated/android/c++/JVariant_NullType_String.hpp +70 -0
  97. package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.cpp +26 -0
  98. package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.hpp +74 -0
  99. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_TrackItem_std__optional_Reason_.kt +80 -0
  100. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_TrackPlayerState_std__optional_Reason_.kt +80 -0
  101. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_bool.kt +80 -0
  102. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_double_double.kt +80 -0
  103. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_double_double_std__optional_bool_.kt +80 -0
  104. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__string_Playlist_std__optional_QueueOperation_.kt +80 -0
  105. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_Playlist__std__optional_QueueOperation_.kt +80 -0
  106. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrarySpec.kt +61 -0
  107. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAudioDevicesSpec.kt +61 -0
  108. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +116 -0
  109. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +134 -0
  110. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/NitroPlayerOnLoad.kt +35 -0
  111. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerConfig.kt +44 -0
  112. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerState.kt +53 -0
  113. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Playlist.kt +50 -0
  114. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/QueueOperation.kt +23 -0
  115. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Reason.kt +23 -0
  116. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TAudioDevice.kt +47 -0
  117. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackItem.kt +56 -0
  118. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackPlayerState.kt +22 -0
  119. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Playlist.kt +59 -0
  120. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_String.kt +59 -0
  121. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_TrackItem.kt +59 -0
  122. package/nitrogen/generated/ios/NitroPlayer+autolinking.rb +60 -0
  123. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +123 -0
  124. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +531 -0
  125. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +80 -0
  126. package/nitrogen/generated/ios/NitroPlayerAutolinking.mm +49 -0
  127. package/nitrogen/generated/ios/NitroPlayerAutolinking.swift +55 -0
  128. package/nitrogen/generated/ios/c++/HybridAudioRoutePickerSpecSwift.cpp +11 -0
  129. package/nitrogen/generated/ios/c++/HybridAudioRoutePickerSpecSwift.hpp +74 -0
  130. package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.cpp +11 -0
  131. package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +167 -0
  132. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.cpp +11 -0
  133. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +174 -0
  134. package/nitrogen/generated/ios/swift/Func_void_TrackItem_std__optional_Reason_.swift +47 -0
  135. package/nitrogen/generated/ios/swift/Func_void_TrackPlayerState_std__optional_Reason_.swift +47 -0
  136. package/nitrogen/generated/ios/swift/Func_void_bool.swift +47 -0
  137. package/nitrogen/generated/ios/swift/Func_void_double_double.swift +47 -0
  138. package/nitrogen/generated/ios/swift/Func_void_double_double_std__optional_bool_.swift +54 -0
  139. package/nitrogen/generated/ios/swift/Func_void_std__string_Playlist_std__optional_QueueOperation_.swift +47 -0
  140. package/nitrogen/generated/ios/swift/Func_void_std__vector_Playlist__std__optional_QueueOperation_.swift +47 -0
  141. package/nitrogen/generated/ios/swift/HybridAudioRoutePickerSpec.swift +56 -0
  142. package/nitrogen/generated/ios/swift/HybridAudioRoutePickerSpec_cxx.swift +130 -0
  143. package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec.swift +68 -0
  144. package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec_cxx.swift +349 -0
  145. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +69 -0
  146. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +325 -0
  147. package/nitrogen/generated/ios/swift/PlayerConfig.swift +115 -0
  148. package/nitrogen/generated/ios/swift/PlayerState.swift +181 -0
  149. package/nitrogen/generated/ios/swift/Playlist.swift +182 -0
  150. package/nitrogen/generated/ios/swift/QueueOperation.swift +48 -0
  151. package/nitrogen/generated/ios/swift/Reason.swift +48 -0
  152. package/nitrogen/generated/ios/swift/TrackItem.swift +147 -0
  153. package/nitrogen/generated/ios/swift/TrackPlayerState.swift +44 -0
  154. package/nitrogen/generated/ios/swift/Variant_NullType_Playlist.swift +18 -0
  155. package/nitrogen/generated/ios/swift/Variant_NullType_String.swift +18 -0
  156. package/nitrogen/generated/ios/swift/Variant_NullType_TrackItem.swift +18 -0
  157. package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.cpp +22 -0
  158. package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.hpp +63 -0
  159. package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.cpp +22 -0
  160. package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.hpp +65 -0
  161. package/nitrogen/generated/shared/c++/HybridAudioRoutePickerSpec.cpp +21 -0
  162. package/nitrogen/generated/shared/c++/HybridAudioRoutePickerSpec.hpp +62 -0
  163. package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.cpp +33 -0
  164. package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.hpp +87 -0
  165. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +34 -0
  166. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +91 -0
  167. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +83 -0
  168. package/nitrogen/generated/shared/c++/PlayerState.hpp +103 -0
  169. package/nitrogen/generated/shared/c++/Playlist.hpp +97 -0
  170. package/nitrogen/generated/shared/c++/QueueOperation.hpp +84 -0
  171. package/nitrogen/generated/shared/c++/Reason.hpp +84 -0
  172. package/nitrogen/generated/shared/c++/TAudioDevice.hpp +87 -0
  173. package/nitrogen/generated/shared/c++/TrackItem.hpp +102 -0
  174. package/nitrogen/generated/shared/c++/TrackPlayerState.hpp +80 -0
  175. package/package.json +172 -0
  176. package/react-native.config.js +16 -0
  177. package/src/hooks/index.ts +6 -0
  178. package/src/hooks/useAndroidAutoConnection.ts +30 -0
  179. package/src/hooks/useAudioDevices.ts +64 -0
  180. package/src/hooks/useOnChangeTrack.ts +24 -0
  181. package/src/hooks/useOnPlaybackProgressChange.ts +30 -0
  182. package/src/hooks/useOnPlaybackStateChange.ts +24 -0
  183. package/src/hooks/useOnSeek.ts +25 -0
  184. package/src/index.ts +47 -0
  185. package/src/specs/AndroidAutoMediaLibrary.nitro.ts +22 -0
  186. package/src/specs/AudioDevices.nitro.ts +25 -0
  187. package/src/specs/AudioRoutePicker.nitro.ts +9 -0
  188. package/src/specs/TrackPlayer.nitro.ts +81 -0
  189. package/src/types/AndroidAutoMediaLibrary.ts +58 -0
  190. package/src/types/PlayerQueue.ts +38 -0
  191. package/src/utils/androidAutoMediaLibrary.ts +66 -0
@@ -0,0 +1,454 @@
1
+ package com.margelo.nitro.nitroplayer.playlist
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+ import com.margelo.nitro.core.NullType
6
+ import com.margelo.nitro.nitroplayer.QueueOperation
7
+ import com.margelo.nitro.nitroplayer.TrackItem
8
+ import com.margelo.nitro.nitroplayer.Variant_NullType_String
9
+ import com.margelo.nitro.nitroplayer.core.TrackPlayerCore
10
+ import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
11
+ import org.json.JSONArray
12
+ import org.json.JSONObject
13
+ import java.util.UUID
14
+ import java.util.concurrent.CopyOnWriteArrayList
15
+
16
+ /**
17
+ * Manages multiple playlists using ExoPlayer's native playlist functionality
18
+ * Based on: https://developer.android.com/media/media3/exoplayer/playlists
19
+ */
20
+ class PlaylistManager private constructor(
21
+ private val context: Context,
22
+ ) {
23
+ private val playlists: MutableMap<String, Playlist> = mutableMapOf()
24
+ private val listeners = CopyOnWriteArrayList<(List<Playlist>, QueueOperation?) -> Unit>()
25
+ private val playlistListeners = mutableMapOf<String, CopyOnWriteArrayList<(Playlist, QueueOperation?) -> Unit>>()
26
+ private var currentPlaylistId: String? = null
27
+
28
+ private val sharedPreferences: SharedPreferences =
29
+ context.getSharedPreferences("NitroPlayerPlaylists", Context.MODE_PRIVATE)
30
+
31
+ companion object {
32
+ @Volatile
33
+ @Suppress("ktlint:standard:property-naming")
34
+ private var INSTANCE: PlaylistManager? = null
35
+
36
+ @JvmStatic
37
+ fun getInstance(context: Context): PlaylistManager =
38
+ INSTANCE ?: synchronized(this) {
39
+ INSTANCE ?: PlaylistManager(context.applicationContext).also { INSTANCE = it }
40
+ }
41
+ }
42
+
43
+ init {
44
+ // Don't load from preferences on init - only load when Android Auto needs it
45
+ }
46
+
47
+ /**
48
+ * Create a new playlist
49
+ */
50
+ fun createPlaylist(
51
+ name: String,
52
+ description: String? = null,
53
+ artwork: String? = null,
54
+ ): String {
55
+ val id = UUID.randomUUID().toString()
56
+ val playlist = Playlist(id, name, description, artwork)
57
+
58
+ synchronized(playlists) {
59
+ playlists[id] = playlist
60
+ }
61
+
62
+ // Only cache for Android Auto if connected
63
+ if (NitroPlayerMediaBrowserService.isAndroidAutoConnected) {
64
+ savePlaylistsToPreferences()
65
+ }
66
+ notifyPlaylistsChanged(QueueOperation.ADD)
67
+ NitroPlayerMediaBrowserService.getInstance()?.onPlaylistsUpdated()
68
+
69
+ return id
70
+ }
71
+
72
+ /**
73
+ * Delete a playlist
74
+ */
75
+ fun deletePlaylist(playlistId: String): Boolean {
76
+ val removed =
77
+ synchronized(playlists) {
78
+ playlists.remove(playlistId)
79
+ }
80
+
81
+ if (removed != null) {
82
+ if (currentPlaylistId == playlistId) {
83
+ currentPlaylistId = null
84
+ }
85
+ playlistListeners.remove(playlistId)
86
+ // Only cache for Android Auto if connected
87
+ if (NitroPlayerMediaBrowserService.isAndroidAutoConnected) {
88
+ savePlaylistsToPreferences()
89
+ }
90
+ notifyPlaylistsChanged(QueueOperation.REMOVE)
91
+ NitroPlayerMediaBrowserService.getInstance()?.onPlaylistsUpdated()
92
+ return true
93
+ }
94
+
95
+ return false
96
+ }
97
+
98
+ /**
99
+ * Update playlist metadata
100
+ */
101
+ fun updatePlaylist(
102
+ playlistId: String,
103
+ name: String? = null,
104
+ description: String? = null,
105
+ artwork: String? = null,
106
+ ): Boolean {
107
+ val playlist =
108
+ synchronized(playlists) {
109
+ playlists[playlistId]
110
+ } ?: return false
111
+
112
+ synchronized(playlists) {
113
+ playlists[playlistId] =
114
+ playlist.copy(
115
+ name = name ?: playlist.name,
116
+ description = description ?: playlist.description,
117
+ artwork = artwork ?: playlist.artwork,
118
+ )
119
+ }
120
+
121
+ // Only cache for Android Auto if connected
122
+ if (NitroPlayerMediaBrowserService.isAndroidAutoConnected) {
123
+ savePlaylistsToPreferences()
124
+ }
125
+ notifyPlaylistChanged(playlistId, QueueOperation.UPDATE)
126
+ notifyPlaylistsChanged(QueueOperation.UPDATE)
127
+ NitroPlayerMediaBrowserService.getInstance()?.onPlaylistsUpdated()
128
+
129
+ return true
130
+ }
131
+
132
+ /**
133
+ * Get a playlist by ID
134
+ */
135
+ fun getPlaylist(playlistId: String): Playlist? =
136
+ synchronized(playlists) {
137
+ playlists[playlistId]
138
+ }
139
+
140
+ /**
141
+ * Get all playlists
142
+ */
143
+ fun getAllPlaylists(): List<Playlist> =
144
+ synchronized(playlists) {
145
+ playlists.values.toList()
146
+ }
147
+
148
+ /**
149
+ * Add a track to a playlist
150
+ */
151
+ fun addTrackToPlaylist(
152
+ playlistId: String,
153
+ track: TrackItem,
154
+ index: Int? = null,
155
+ ): Boolean {
156
+ val playlist =
157
+ synchronized(playlists) {
158
+ playlists[playlistId]
159
+ } ?: return false
160
+
161
+ synchronized(playlists) {
162
+ val tracks = playlist.tracks.toMutableList()
163
+ if (index != null && index >= 0 && index <= tracks.size) {
164
+ tracks.add(index, track)
165
+ } else {
166
+ tracks.add(track)
167
+ }
168
+ playlists[playlistId] = playlist.copy(tracks = tracks)
169
+ }
170
+
171
+ // Only cache for Android Auto if connected
172
+ if (NitroPlayerMediaBrowserService.isAndroidAutoConnected) {
173
+ savePlaylistsToPreferences()
174
+ }
175
+ notifyPlaylistChanged(playlistId, QueueOperation.ADD)
176
+ NitroPlayerMediaBrowserService.getInstance()?.onPlaylistUpdated(playlistId)
177
+
178
+ // Update ExoPlayer if this is the current playlist
179
+ if (currentPlaylistId == playlistId) {
180
+ TrackPlayerCore.getInstance(context)?.updatePlaylist(playlistId)
181
+ }
182
+
183
+ return true
184
+ }
185
+
186
+ /**
187
+ * Add multiple tracks to a playlist at once
188
+ */
189
+ fun addTracksToPlaylist(
190
+ playlistId: String,
191
+ tracks: List<TrackItem>,
192
+ index: Int? = null,
193
+ ): Boolean {
194
+ val playlist =
195
+ synchronized(playlists) {
196
+ playlists[playlistId]
197
+ } ?: return false
198
+
199
+ synchronized(playlists) {
200
+ val currentTracks = playlist.tracks.toMutableList()
201
+ if (index != null && index >= 0 && index <= currentTracks.size) {
202
+ currentTracks.addAll(index, tracks)
203
+ } else {
204
+ currentTracks.addAll(tracks)
205
+ }
206
+ playlists[playlistId] = playlist.copy(tracks = currentTracks)
207
+ }
208
+
209
+ // Only cache for Android Auto if connected
210
+ if (NitroPlayerMediaBrowserService.isAndroidAutoConnected) {
211
+ savePlaylistsToPreferences()
212
+ }
213
+ notifyPlaylistChanged(playlistId, QueueOperation.ADD)
214
+ NitroPlayerMediaBrowserService.getInstance()?.onPlaylistUpdated(playlistId)
215
+
216
+ // Update ExoPlayer if this is the current playlist
217
+ if (currentPlaylistId == playlistId) {
218
+ TrackPlayerCore.getInstance(context)?.updatePlaylist(playlistId)
219
+ }
220
+
221
+ return true
222
+ }
223
+
224
+ /**
225
+ * Remove a track from a playlist
226
+ */
227
+ fun removeTrackFromPlaylist(
228
+ playlistId: String,
229
+ trackId: String,
230
+ ): Boolean {
231
+ val playlist =
232
+ synchronized(playlists) {
233
+ playlists[playlistId]
234
+ } ?: return false
235
+
236
+ val removed =
237
+ synchronized(playlists) {
238
+ val tracks = playlist.tracks.toMutableList()
239
+ val removed = tracks.removeAll { it.id == trackId }
240
+ if (removed) {
241
+ playlists[playlistId] = playlist.copy(tracks = tracks)
242
+ }
243
+ removed
244
+ }
245
+
246
+ if (removed) {
247
+ savePlaylistsToPreferences()
248
+ notifyPlaylistChanged(playlistId, QueueOperation.REMOVE)
249
+ NitroPlayerMediaBrowserService.getInstance()?.onPlaylistUpdated(playlistId)
250
+
251
+ // Update ExoPlayer if this is the current playlist
252
+ if (currentPlaylistId == playlistId) {
253
+ TrackPlayerCore.getInstance(context)?.updatePlaylist(playlistId)
254
+ }
255
+ }
256
+
257
+ return removed
258
+ }
259
+
260
+ /**
261
+ * Reorder a track in a playlist
262
+ */
263
+ fun reorderTrackInPlaylist(
264
+ playlistId: String,
265
+ trackId: String,
266
+ newIndex: Int,
267
+ ): Boolean {
268
+ val playlist =
269
+ synchronized(playlists) {
270
+ playlists[playlistId]
271
+ } ?: return false
272
+
273
+ val tracks = playlist.tracks.toMutableList()
274
+ val oldIndex = tracks.indexOfFirst { it.id == trackId }
275
+ if (oldIndex < 0 || newIndex < 0 || newIndex >= tracks.size) {
276
+ return false
277
+ }
278
+
279
+ synchronized(playlists) {
280
+ val track = tracks.removeAt(oldIndex)
281
+ tracks.add(newIndex, track)
282
+ playlists[playlistId] = playlist.copy(tracks = tracks)
283
+ }
284
+
285
+ savePlaylistsToPreferences()
286
+ notifyPlaylistChanged(playlistId, QueueOperation.UPDATE)
287
+ NitroPlayerMediaBrowserService.getInstance()?.onPlaylistUpdated(playlistId)
288
+
289
+ // Update ExoPlayer if this is the current playlist
290
+ if (currentPlaylistId == playlistId) {
291
+ TrackPlayerCore.getInstance(context)?.updatePlaylist(playlistId)
292
+ }
293
+
294
+ return true
295
+ }
296
+
297
+ /**
298
+ * Load a playlist for playback (sets it as current)
299
+ */
300
+ fun loadPlaylist(playlistId: String): Boolean {
301
+ val playlist =
302
+ synchronized(playlists) {
303
+ playlists[playlistId]
304
+ } ?: return false
305
+
306
+ currentPlaylistId = playlistId
307
+ TrackPlayerCore.getInstance(context)?.loadPlaylist(playlistId)
308
+
309
+ return true
310
+ }
311
+
312
+ /**
313
+ * Get the current playlist ID
314
+ */
315
+ fun getCurrentPlaylistId(): String? = currentPlaylistId
316
+
317
+ /**
318
+ * Get the current playlist
319
+ */
320
+ fun getCurrentPlaylist(): Playlist? = currentPlaylistId?.let { synchronized(playlists) { playlists[it] } }
321
+
322
+ /**
323
+ * Add a listener for playlist changes
324
+ */
325
+ fun addPlaylistsChangeListener(listener: (List<Playlist>, QueueOperation?) -> Unit): () -> Unit {
326
+ listeners.add(listener)
327
+ return { listeners.remove(listener) }
328
+ }
329
+
330
+ /**
331
+ * Add a listener for a specific playlist changes
332
+ */
333
+ fun addPlaylistChangeListener(
334
+ playlistId: String,
335
+ listener: (Playlist, QueueOperation?) -> Unit,
336
+ ): () -> Unit {
337
+ val playlistListeners = playlistListeners.getOrPut(playlistId) { CopyOnWriteArrayList() }
338
+ playlistListeners.add(listener)
339
+ return { playlistListeners.remove(listener) }
340
+ }
341
+
342
+ private fun notifyPlaylistsChanged(operation: QueueOperation?) {
343
+ val allPlaylists =
344
+ synchronized(playlists) {
345
+ playlists.values.toList()
346
+ }
347
+ listeners.forEach { it(allPlaylists, operation) }
348
+ }
349
+
350
+ private fun notifyPlaylistChanged(
351
+ playlistId: String,
352
+ operation: QueueOperation?,
353
+ ) {
354
+ val playlist =
355
+ synchronized(playlists) {
356
+ playlists[playlistId]
357
+ } ?: return
358
+
359
+ playlistListeners[playlistId]?.forEach { it(playlist, operation) }
360
+ }
361
+
362
+ private fun savePlaylistsToPreferences() {
363
+ try {
364
+ val jsonArray = JSONArray()
365
+ synchronized(playlists) {
366
+ playlists.values.forEach { playlist ->
367
+ val jsonObject =
368
+ JSONObject().apply {
369
+ put("id", playlist.id)
370
+ put("name", playlist.name)
371
+ put("description", playlist.description ?: "")
372
+ put("artwork", playlist.artwork ?: "")
373
+ val tracksArray = JSONArray()
374
+ playlist.tracks.forEach { track ->
375
+ tracksArray.put(
376
+ JSONObject().apply {
377
+ put("id", track.id)
378
+ put("title", track.title)
379
+ put("artist", track.artist)
380
+ put("album", track.album)
381
+ put("duration", track.duration)
382
+ put("url", track.url)
383
+ track.artwork?.let { put("artwork", it) }
384
+ },
385
+ )
386
+ }
387
+ put("tracks", tracksArray)
388
+ }
389
+ jsonArray.put(jsonObject)
390
+ }
391
+ }
392
+
393
+ sharedPreferences
394
+ .edit()
395
+ .putString("playlists", jsonArray.toString())
396
+ .putString("currentPlaylistId", currentPlaylistId)
397
+ .apply()
398
+ } catch (e: Exception) {
399
+ e.printStackTrace()
400
+ }
401
+ }
402
+
403
+ private fun loadPlaylistsFromPreferences() {
404
+ try {
405
+ val jsonString = sharedPreferences.getString("playlists", null)
406
+ if (jsonString != null) {
407
+ val jsonArray = JSONArray(jsonString)
408
+ synchronized(playlists) {
409
+ playlists.clear()
410
+ for (i in 0 until jsonArray.length()) {
411
+ val jsonObject = jsonArray.getJSONObject(i)
412
+ val tracks = mutableListOf<TrackItem>()
413
+ val tracksArray = jsonObject.getJSONArray("tracks")
414
+ for (j in 0 until tracksArray.length()) {
415
+ val trackObj = tracksArray.getJSONObject(j)
416
+ val artworkStr = trackObj.optString("artwork")
417
+ val artwork: Variant_NullType_String? =
418
+ if (!artworkStr.isNullOrEmpty()) {
419
+ Variant_NullType_String.create(artworkStr)
420
+ } else {
421
+ null
422
+ }
423
+ tracks.add(
424
+ TrackItem(
425
+ id = trackObj.getString("id"),
426
+ title = trackObj.getString("title"),
427
+ artist = trackObj.getString("artist"),
428
+ album = trackObj.getString("album"),
429
+ duration = trackObj.getDouble("duration"),
430
+ url = trackObj.getString("url"),
431
+ artwork = artwork,
432
+ ),
433
+ )
434
+ }
435
+ val descriptionStr = jsonObject.optString("description")
436
+ val artworkStr = jsonObject.optString("artwork")
437
+ val playlist =
438
+ Playlist(
439
+ id = jsonObject.getString("id"),
440
+ name = jsonObject.getString("name"),
441
+ description = if (!descriptionStr.isNullOrEmpty()) descriptionStr else null,
442
+ artwork = if (!artworkStr.isNullOrEmpty()) artworkStr else null,
443
+ tracks = tracks,
444
+ )
445
+ playlists[playlist.id] = playlist
446
+ }
447
+ }
448
+ currentPlaylistId = sharedPreferences.getString("currentPlaylistId", null)
449
+ }
450
+ } catch (e: Exception) {
451
+ e.printStackTrace()
452
+ }
453
+ }
454
+ }
@@ -0,0 +1,94 @@
1
+ package com.margelo.nitro.nitroplayer.queue
2
+
3
+ import com.margelo.nitro.nitroplayer.TrackItem
4
+ import java.util.concurrent.CopyOnWriteArrayList
5
+
6
+ /**
7
+ * Queue class that manages a list of tracks.
8
+ * Thread-safe implementation using CopyOnWriteArrayList.
9
+ */
10
+ class Queue {
11
+ private val tracks: MutableList<TrackItem> = CopyOnWriteArrayList()
12
+
13
+ /**
14
+ * Get all tracks in the queue
15
+ */
16
+ fun getTracks(): List<TrackItem> = tracks.toList()
17
+
18
+ /**
19
+ * Get tracks as an array (for compatibility with existing API)
20
+ */
21
+ fun getTracksArray(): Array<TrackItem> = tracks.toTypedArray()
22
+
23
+ /**
24
+ * Add a single track to the queue
25
+ */
26
+ fun addTrack(track: TrackItem) {
27
+ tracks.add(track)
28
+ }
29
+
30
+ /**
31
+ * Add a track at a specific index
32
+ */
33
+ fun addTrackAtIndex(
34
+ track: TrackItem,
35
+ index: Int,
36
+ ) {
37
+ if (index < 0 || index > tracks.size) {
38
+ tracks.add(track)
39
+ } else {
40
+ tracks.add(index, track)
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Load multiple tracks into the queue (replaces existing queue)
46
+ */
47
+ fun loadTracks(newTracks: List<TrackItem>) {
48
+ tracks.clear()
49
+ tracks.addAll(newTracks)
50
+ }
51
+
52
+ /**
53
+ * Remove a track by ID
54
+ * @return true if track was found and removed, false otherwise
55
+ */
56
+ fun removeTrack(id: String): Boolean = tracks.removeAll { it.id == id }
57
+
58
+ /**
59
+ * Clear all tracks from the queue
60
+ */
61
+ fun clear() {
62
+ tracks.clear()
63
+ }
64
+
65
+ /**
66
+ * Get the size of the queue
67
+ */
68
+ fun size(): Int = tracks.size
69
+
70
+ /**
71
+ * Check if the queue is empty
72
+ */
73
+ fun isEmpty(): Boolean = tracks.isEmpty()
74
+
75
+ /**
76
+ * Get a track by index
77
+ */
78
+ fun getTrack(index: Int): TrackItem? =
79
+ if (index >= 0 && index < tracks.size) {
80
+ tracks[index]
81
+ } else {
82
+ null
83
+ }
84
+
85
+ /**
86
+ * Get a track by ID
87
+ */
88
+ fun getTrackById(id: String): TrackItem? = tracks.find { it.id == id }
89
+
90
+ /**
91
+ * Get the index of a track by ID
92
+ */
93
+ fun getTrackIndex(id: String): Int = tracks.indexOfFirst { it.id == id }
94
+ }
@@ -0,0 +1,143 @@
1
+ package com.margelo.nitro.nitroplayer.queue
2
+
3
+ import com.margelo.nitro.nitroplayer.QueueOperation
4
+ import com.margelo.nitro.nitroplayer.TrackItem
5
+ import java.util.concurrent.CopyOnWriteArrayList
6
+
7
+ /**
8
+ * QueueManager is a singleton that manages the queue state across the app session.
9
+ * It provides thread-safe access to the queue and notifies listeners of queue changes.
10
+ */
11
+ class QueueManager private constructor() {
12
+ private val queue = Queue()
13
+ private val listeners = CopyOnWriteArrayList<(List<TrackItem>, QueueOperation?) -> Unit>()
14
+
15
+ companion object {
16
+ @Volatile
17
+ @Suppress("ktlint:standard:property-naming")
18
+ private var INSTANCE: QueueManager? = null
19
+
20
+ /**
21
+ * Get the singleton instance of QueueManager
22
+ */
23
+ @JvmStatic
24
+ fun getInstance(): QueueManager =
25
+ INSTANCE ?: synchronized(this) {
26
+ INSTANCE ?: QueueManager().also { INSTANCE = it }
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Get the current queue
32
+ */
33
+ fun getQueue(): Queue = queue
34
+
35
+ /**
36
+ * Get all tracks in the queue
37
+ */
38
+ fun getTracks(): List<TrackItem> = queue.getTracks()
39
+
40
+ /**
41
+ * Get tracks as an array
42
+ */
43
+ fun getTracksArray(): Array<TrackItem> = queue.getTracksArray()
44
+
45
+ /**
46
+ * Load multiple tracks into the queue (replaces existing queue)
47
+ */
48
+ fun loadQueue(tracks: Array<TrackItem>) {
49
+ queue.loadTracks(tracks.toList())
50
+ notifyListeners(QueueOperation.ADD)
51
+ }
52
+
53
+ /**
54
+ * Load a single track at a specific index
55
+ */
56
+ fun loadSingleTrack(
57
+ track: TrackItem,
58
+ index: Double?,
59
+ ) {
60
+ val insertIndex = index?.toInt()
61
+ if (insertIndex != null && insertIndex >= 0) {
62
+ queue.addTrackAtIndex(track, insertIndex)
63
+ } else {
64
+ queue.addTrack(track)
65
+ }
66
+ notifyListeners(QueueOperation.ADD)
67
+ }
68
+
69
+ /**
70
+ * Delete a track by ID
71
+ */
72
+ fun deleteTrack(id: String) {
73
+ val removed = queue.removeTrack(id)
74
+ if (removed) {
75
+ notifyListeners(QueueOperation.REMOVE)
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Clear all tracks from the queue
81
+ */
82
+ fun clearQueue() {
83
+ queue.clear()
84
+ notifyListeners(QueueOperation.CLEAR)
85
+ }
86
+
87
+ /**
88
+ * Add a listener for queue changes
89
+ * @param listener Callback that receives (queue, operation)
90
+ * @return A function to remove the listener
91
+ */
92
+ fun addQueueChangeListener(listener: (List<TrackItem>, QueueOperation?) -> Unit): () -> Unit {
93
+ listeners.add(listener)
94
+ return { listeners.remove(listener) }
95
+ }
96
+
97
+ /**
98
+ * Remove a queue change listener
99
+ */
100
+ fun removeQueueChangeListener(listener: (List<TrackItem>, QueueOperation?) -> Unit) {
101
+ listeners.remove(listener)
102
+ }
103
+
104
+ /**
105
+ * Notify all listeners of queue changes
106
+ */
107
+ private fun notifyListeners(operation: QueueOperation?) {
108
+ val currentTracks = queue.getTracks()
109
+ listeners.forEach { listener ->
110
+ try {
111
+ listener(currentTracks, operation)
112
+ } catch (e: Exception) {
113
+ // Log error but don't break other listeners
114
+ e.printStackTrace()
115
+ }
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Get queue size
121
+ */
122
+ fun getQueueSize(): Int = queue.size()
123
+
124
+ /**
125
+ * Check if queue is empty
126
+ */
127
+ fun isQueueEmpty(): Boolean = queue.isEmpty()
128
+
129
+ /**
130
+ * Get a track by index
131
+ */
132
+ fun getTrack(index: Int): TrackItem? = queue.getTrack(index)
133
+
134
+ /**
135
+ * Get a track by ID
136
+ */
137
+ fun getTrackById(id: String): TrackItem? = queue.getTrackById(id)
138
+
139
+ /**
140
+ * Get the index of a track by ID
141
+ */
142
+ fun getTrackIndex(id: String): Int = queue.getTrackIndex(id)
143
+ }