@5stones/react-native-audio-browser 0.1.4 → 0.1.6
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/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +11 -71
- package/android/src/main/java/com/audiobrowser/player/TransformingDataSource.kt +125 -0
- package/ios/Browser/BrowserManager.swift +17 -2
- package/ios/TrackPlayer.swift +145 -31
- package/lib/commonjs/features/browser.js.map +1 -1
- package/lib/commonjs/features/equalizer.js.map +1 -1
- package/lib/commonjs/features/errors.js.map +1 -1
- package/lib/commonjs/features/favorites.js.map +1 -1
- package/lib/commonjs/features/metadata.js.map +1 -1
- package/lib/commonjs/features/nowPlaying.js.map +1 -1
- package/lib/commonjs/features/output.js.map +1 -1
- package/lib/commonjs/features/playback/state.js.map +1 -1
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
- package/lib/commonjs/features/queue/queue.js +1 -1
- package/lib/commonjs/features/queue/queue.js.map +1 -1
- package/lib/commonjs/features/remoteControls.js.map +1 -1
- package/lib/commonjs/utils/useDebug.js.map +1 -1
- package/lib/commonjs/utils/validation.js +23 -0
- package/lib/commonjs/utils/validation.js.map +1 -0
- package/lib/commonjs/web/NativeAudioBrowser.js +53 -15
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/SimpleRouter.js +5 -4
- package/lib/commonjs/web/SimpleRouter.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/Player.js +49 -24
- package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/PlaylistPlayer.js +54 -45
- package/lib/commonjs/web/TrackPlayer/PlaylistPlayer.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/State.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +10 -17
- package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
- package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
- package/lib/commonjs/web/browser/NavigationErrorManager.js.map +1 -1
- package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
- package/lib/commonjs/web/http/HttpClient.js +15 -2
- package/lib/commonjs/web/http/HttpClient.js.map +1 -1
- package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
- package/lib/commonjs/web/player/NowPlayingManager.js +9 -0
- package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
- package/lib/commonjs/web/player/OptionsManager.js +1 -1
- package/lib/commonjs/web/player/OptionsManager.js.map +1 -1
- package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/module/features/browser.js.map +1 -1
- package/lib/module/features/equalizer.js.map +1 -1
- package/lib/module/features/errors.js.map +1 -1
- package/lib/module/features/favorites.js.map +1 -1
- package/lib/module/features/metadata.js.map +1 -1
- package/lib/module/features/nowPlaying.js +1 -0
- package/lib/module/features/nowPlaying.js.map +1 -1
- package/lib/module/features/output.js.map +1 -1
- package/lib/module/features/playback/state.js.map +1 -1
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/queue/activeTrack.js.map +1 -1
- package/lib/module/features/queue/queue.js +1 -1
- package/lib/module/features/queue/queue.js.map +1 -1
- package/lib/module/features/remoteControls.js.map +1 -1
- package/lib/module/utils/useDebug.js.map +1 -1
- package/lib/module/utils/validation.js +18 -0
- package/lib/module/utils/validation.js.map +1 -0
- package/lib/module/web/NativeAudioBrowser.js +53 -15
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/SimpleRouter.js +5 -4
- package/lib/module/web/SimpleRouter.js.map +1 -1
- package/lib/module/web/TrackPlayer/Player.js +49 -24
- package/lib/module/web/TrackPlayer/Player.js.map +1 -1
- package/lib/module/web/TrackPlayer/PlaylistPlayer.js +54 -45
- package/lib/module/web/TrackPlayer/PlaylistPlayer.js.map +1 -1
- package/lib/module/web/TrackPlayer/State.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +10 -17
- package/lib/module/web/browser/BrowserManager.js.map +1 -1
- package/lib/module/web/browser/FavoriteManager.js.map +1 -1
- package/lib/module/web/browser/NavigationErrorManager.js.map +1 -1
- package/lib/module/web/browser/SearchManager.js.map +1 -1
- package/lib/module/web/http/HttpClient.js +15 -2
- package/lib/module/web/http/HttpClient.js.map +1 -1
- package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
- package/lib/module/web/player/NowPlayingManager.js +9 -0
- package/lib/module/web/player/NowPlayingManager.js.map +1 -1
- package/lib/module/web/player/OptionsManager.js +1 -1
- package/lib/module/web/player/OptionsManager.js.map +1 -1
- package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/typescript/src/features/browser.d.ts.map +1 -1
- package/lib/typescript/src/features/equalizer.d.ts.map +1 -1
- package/lib/typescript/src/features/errors.d.ts.map +1 -1
- package/lib/typescript/src/features/favorites.d.ts.map +1 -1
- package/lib/typescript/src/features/metadata.d.ts +1 -1
- package/lib/typescript/src/features/metadata.d.ts.map +1 -1
- package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
- package/lib/typescript/src/features/nowPlaying.d.ts.map +1 -1
- package/lib/typescript/src/features/output.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/state.d.ts +1 -1
- package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
- package/lib/typescript/src/features/player/options.d.ts +1 -1
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
- package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
- package/lib/typescript/src/specs/audio-browser.nitro.d.ts.map +1 -1
- package/lib/typescript/src/utils/useDebug.d.ts +1 -1
- package/lib/typescript/src/utils/useDebug.d.ts.map +1 -1
- package/lib/typescript/src/utils/validation.d.ts +3 -0
- package/lib/typescript/src/utils/validation.d.ts.map +1 -0
- package/lib/typescript/src/web/NativeAudioBrowser.d.ts +6 -1
- package/lib/typescript/src/web/NativeAudioBrowser.d.ts.map +1 -1
- package/lib/typescript/src/web/SimpleRouter.d.ts.map +1 -1
- package/lib/typescript/src/web/TrackPlayer/Player.d.ts +7 -19
- package/lib/typescript/src/web/TrackPlayer/Player.d.ts.map +1 -1
- package/lib/typescript/src/web/TrackPlayer/PlaylistPlayer.d.ts +3 -3
- package/lib/typescript/src/web/TrackPlayer/PlaylistPlayer.d.ts.map +1 -1
- package/lib/typescript/src/web/browser/BrowserManager.d.ts.map +1 -1
- package/lib/typescript/src/web/browser/FavoriteManager.d.ts.map +1 -1
- package/lib/typescript/src/web/browser/NavigationErrorManager.d.ts.map +1 -1
- package/lib/typescript/src/web/browser/SearchManager.d.ts +1 -1
- package/lib/typescript/src/web/browser/SearchManager.d.ts.map +1 -1
- package/lib/typescript/src/web/http/HttpClient.d.ts.map +1 -1
- package/lib/typescript/src/web/http/RequestConfigBuilder.d.ts.map +1 -1
- package/lib/typescript/src/web/player/NowPlayingManager.d.ts.map +1 -1
- package/lib/typescript/src/web/player/OptionsManager.d.ts.map +1 -1
- package/lib/typescript/src/web/util/BrowserPathHelper.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/features/browser.ts +1 -1
- package/src/features/equalizer.ts +1 -1
- package/src/features/errors.ts +1 -1
- package/src/features/favorites.ts +1 -1
- package/src/features/metadata.ts +1 -1
- package/src/features/nowPlaying.ts +1 -1
- package/src/features/output.ts +1 -1
- package/src/features/playback/state.ts +1 -1
- package/src/features/player/options.ts +2 -2
- package/src/features/queue/activeTrack.ts +1 -1
- package/src/features/queue/queue.ts +2 -2
- package/src/features/remoteControls.ts +2 -2
- package/src/specs/audio-browser.nitro.ts +0 -1
- package/src/utils/useDebug.ts +6 -6
- package/src/utils/validation.ts +27 -0
- package/src/web/NativeAudioBrowser.ts +137 -58
- package/src/web/SimpleRouter.ts +24 -9
- package/src/web/TrackPlayer/Player.ts +58 -30
- package/src/web/TrackPlayer/PlaylistPlayer.ts +72 -63
- package/src/web/TrackPlayer/RepeatMode.ts +1 -1
- package/src/web/TrackPlayer/State.ts +9 -9
- package/src/web/browser/BrowserManager.ts +124 -67
- package/src/web/browser/FavoriteManager.ts +5 -3
- package/src/web/browser/NavigationErrorManager.ts +15 -8
- package/src/web/browser/SearchManager.ts +17 -11
- package/src/web/http/HttpClient.ts +25 -7
- package/src/web/http/RequestConfigBuilder.ts +29 -13
- package/src/web/player/NowPlayingManager.ts +13 -7
- package/src/web/player/OptionsManager.ts +7 -6
- package/src/web/util/BrowserPathHelper.ts +3 -2
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { Player } from './Player'
|
|
2
|
-
import { State } from './State'
|
|
3
|
-
import type { State as StateType } from './State'
|
|
4
|
-
|
|
5
|
-
import type { Track } from '../../types'
|
|
6
1
|
import type { RepeatMode as RepeatModeType } from '../../features'
|
|
7
|
-
import {
|
|
2
|
+
import type { Track } from '../../types'
|
|
3
|
+
import type { State as StateType } from './State'
|
|
4
|
+
import { assertedNotNullish } from '../../utils/validation'
|
|
8
5
|
import { fisherYatesShuffle } from '../util/shuffle'
|
|
6
|
+
import { Player } from './Player'
|
|
7
|
+
import { RepeatMode } from './RepeatMode'
|
|
8
|
+
import { State } from './State'
|
|
9
9
|
|
|
10
10
|
export class PlaylistPlayer extends Player {
|
|
11
11
|
// TODO: use immer to make the `playlist` immutable
|
|
@@ -19,6 +19,8 @@ export class PlaylistPlayer extends Player {
|
|
|
19
19
|
protected onStateUpdate(state: Exclude<StateType, typeof State.Error>) {
|
|
20
20
|
super.onStateUpdate(state)
|
|
21
21
|
|
|
22
|
+
if (this._isStopped) return
|
|
23
|
+
|
|
22
24
|
if (state === State.Ended) {
|
|
23
25
|
this.onTrackEnded()
|
|
24
26
|
}
|
|
@@ -63,19 +65,15 @@ export class PlaylistPlayer extends Player {
|
|
|
63
65
|
if (!track) return
|
|
64
66
|
|
|
65
67
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
66
|
-
const
|
|
68
|
+
const onLoaded = (_track: Track) => {
|
|
67
69
|
if (initialPosition !== undefined) {
|
|
68
70
|
this.seekTo(initialPosition)
|
|
69
71
|
}
|
|
70
|
-
|
|
71
|
-
if (this.playWhenReady) {
|
|
72
|
-
this.play()
|
|
73
|
-
}
|
|
74
72
|
}
|
|
75
73
|
|
|
76
74
|
if (this.currentIndex !== index) {
|
|
77
75
|
this.currentIndex = index
|
|
78
|
-
this.load(track,
|
|
76
|
+
this.load(track, onLoaded)
|
|
79
77
|
} else {
|
|
80
78
|
// Replay the same track - seek to start (or initialPosition if specified)
|
|
81
79
|
this.seekTo(initialPosition ?? 0)
|
|
@@ -124,7 +122,8 @@ export class PlaylistPlayer extends Player {
|
|
|
124
122
|
public skipToPrevious(initialPosition?: number): void {
|
|
125
123
|
if (this.currentIndex === undefined) return
|
|
126
124
|
|
|
127
|
-
const previousIndex =
|
|
125
|
+
const previousIndex =
|
|
126
|
+
this.getPreviousIndex() ?? this.getWrapAroundLastIndex()
|
|
128
127
|
if (previousIndex === undefined) return
|
|
129
128
|
|
|
130
129
|
this.goToIndex(previousIndex, initialPosition)
|
|
@@ -184,13 +183,17 @@ export class PlaylistPlayer extends Player {
|
|
|
184
183
|
|
|
185
184
|
protected getWrapAroundLastIndex(): number | undefined {
|
|
186
185
|
if (this.repeatMode !== RepeatMode.Playlist) return undefined
|
|
187
|
-
if (this.shuffleEnabled)
|
|
186
|
+
if (this.shuffleEnabled)
|
|
187
|
+
return this.shuffleOrder[this.shuffleOrder.length - 1]
|
|
188
188
|
return this.playlist.length - 1
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
protected generateShuffleOrder(): void {
|
|
192
192
|
// Create array of indices [0, 1, 2, ..., n-1]
|
|
193
|
-
this.shuffleOrder = Array.from(
|
|
193
|
+
this.shuffleOrder = Array.from(
|
|
194
|
+
{ length: this.playlist.length },
|
|
195
|
+
(_, i) => i
|
|
196
|
+
)
|
|
194
197
|
|
|
195
198
|
// Shuffle the indices
|
|
196
199
|
fisherYatesShuffle(this.shuffleOrder)
|
|
@@ -203,8 +206,8 @@ export class PlaylistPlayer extends Player {
|
|
|
203
206
|
// If there is a current track, move it to the beginning of the shuffle order
|
|
204
207
|
const currentPos = this.shuffleOrder.indexOf(this.currentIndex)
|
|
205
208
|
if (currentPos > 0) {
|
|
206
|
-
const temp = this.shuffleOrder[0]
|
|
207
|
-
this.shuffleOrder[0] = this.shuffleOrder[currentPos]
|
|
209
|
+
const temp = assertedNotNullish(this.shuffleOrder[0])
|
|
210
|
+
this.shuffleOrder[0] = assertedNotNullish(this.shuffleOrder[currentPos])
|
|
208
211
|
this.shuffleOrder[currentPos] = temp
|
|
209
212
|
}
|
|
210
213
|
}
|
|
@@ -236,16 +239,19 @@ export class PlaylistPlayer extends Player {
|
|
|
236
239
|
}
|
|
237
240
|
|
|
238
241
|
public remove(indexes: number[]): void {
|
|
239
|
-
const
|
|
240
|
-
acc[elem] = true
|
|
241
|
-
return acc
|
|
242
|
-
}, {})
|
|
242
|
+
const idxSet = new Set(indexes)
|
|
243
243
|
let isCurrentRemoved = false
|
|
244
|
+
let removedBeforeCurrent = 0
|
|
245
|
+
|
|
244
246
|
this.playlist = this.playlist.filter((_track, idx) => {
|
|
245
|
-
const keep = !
|
|
247
|
+
const keep = !idxSet.has(idx)
|
|
246
248
|
|
|
247
|
-
if (!keep
|
|
248
|
-
|
|
249
|
+
if (!keep) {
|
|
250
|
+
if (idx === this.currentIndex) {
|
|
251
|
+
isCurrentRemoved = true
|
|
252
|
+
} else if (this.currentIndex !== undefined && idx < this.currentIndex) {
|
|
253
|
+
removedBeforeCurrent++
|
|
254
|
+
}
|
|
249
255
|
}
|
|
250
256
|
|
|
251
257
|
return keep
|
|
@@ -255,11 +261,22 @@ export class PlaylistPlayer extends Player {
|
|
|
255
261
|
return
|
|
256
262
|
}
|
|
257
263
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
264
|
+
if (isCurrentRemoved) {
|
|
265
|
+
const hasItems = this.playlist.length > 0
|
|
266
|
+
if (hasItems) {
|
|
267
|
+
// Adjust for removed items before current, then clamp to valid range
|
|
268
|
+
const adjustedIndex = this.currentIndex - removedBeforeCurrent
|
|
269
|
+
// Reset so goToIndex always loads the new track at this position
|
|
270
|
+
this._currentIndex = undefined
|
|
271
|
+
this.goToIndex(Math.min(adjustedIndex, this.playlist.length - 1))
|
|
272
|
+
} else {
|
|
273
|
+
this.current = undefined
|
|
274
|
+
this._currentIndex = undefined
|
|
275
|
+
this.stop()
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
// Adjust currentIndex to account for removed items before it
|
|
279
|
+
this._currentIndex = this.currentIndex - removedBeforeCurrent
|
|
263
280
|
}
|
|
264
281
|
|
|
265
282
|
// Regenerate shuffle order when tracks are removed
|
|
@@ -269,16 +286,17 @@ export class PlaylistPlayer extends Player {
|
|
|
269
286
|
}
|
|
270
287
|
|
|
271
288
|
public stop(onComplete?: () => void): void {
|
|
272
|
-
super.stop(
|
|
273
|
-
this.currentIndex = undefined
|
|
274
|
-
onComplete?.()
|
|
275
|
-
})
|
|
289
|
+
super.stop(onComplete)
|
|
276
290
|
}
|
|
277
291
|
|
|
278
292
|
public reset(): void {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
293
|
+
// Clear queue state synchronously so subsequent add()/load() calls
|
|
294
|
+
// see a clean slate. The async player.unload() in stop() can finish
|
|
295
|
+
// in the background — it only releases the Shaka source.
|
|
296
|
+
this.playlist = []
|
|
297
|
+
this.current = undefined
|
|
298
|
+
this._currentIndex = undefined
|
|
299
|
+
this.stop()
|
|
282
300
|
}
|
|
283
301
|
|
|
284
302
|
public removeUpcomingTracks(): void {
|
|
@@ -296,37 +314,28 @@ export class PlaylistPlayer extends Player {
|
|
|
296
314
|
throw new Error('index out of bounds')
|
|
297
315
|
}
|
|
298
316
|
|
|
299
|
-
|
|
300
|
-
throw new Error('you cannot move the currently playing track')
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (this.currentIndex === toIndex) {
|
|
304
|
-
throw new Error('you cannot replace the currently playing track')
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// calculate `currentIndex` after move
|
|
308
|
-
let shift: number | undefined
|
|
309
|
-
if (
|
|
310
|
-
this.currentIndex !== undefined &&
|
|
311
|
-
fromIndex < this.currentIndex &&
|
|
312
|
-
toIndex > this.currentIndex
|
|
313
|
-
) {
|
|
314
|
-
shift = -1
|
|
315
|
-
} else if (
|
|
316
|
-
this.currentIndex !== undefined &&
|
|
317
|
-
fromIndex > this.currentIndex &&
|
|
318
|
-
toIndex < this.currentIndex
|
|
319
|
-
) {
|
|
320
|
-
shift = 1
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// move the track
|
|
317
|
+
// Move the track in the playlist
|
|
324
318
|
const fromItem = this.playlist[fromIndex]
|
|
325
319
|
this.playlist.splice(fromIndex, 1)
|
|
326
320
|
this.playlist.splice(toIndex, 0, fromItem)
|
|
327
321
|
|
|
328
|
-
|
|
329
|
-
|
|
322
|
+
// Update currentIndex to track the currently playing item's new position.
|
|
323
|
+
// Matches Android's exoPlayer.moveMediaItem() which has no restrictions.
|
|
324
|
+
if (this.currentIndex !== undefined) {
|
|
325
|
+
if (fromIndex === this.currentIndex) {
|
|
326
|
+
// Moving the current track — follow it to its new position
|
|
327
|
+
this._currentIndex = toIndex
|
|
328
|
+
} else if (
|
|
329
|
+
fromIndex < this.currentIndex &&
|
|
330
|
+
toIndex >= this.currentIndex
|
|
331
|
+
) {
|
|
332
|
+
this._currentIndex = this.currentIndex - 1
|
|
333
|
+
} else if (
|
|
334
|
+
fromIndex > this.currentIndex &&
|
|
335
|
+
toIndex <= this.currentIndex
|
|
336
|
+
) {
|
|
337
|
+
this._currentIndex = this.currentIndex + 1
|
|
338
|
+
}
|
|
330
339
|
}
|
|
331
340
|
|
|
332
341
|
// Regenerate shuffle order when tracks are moved
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { PlaybackState } from '../../features'
|
|
2
2
|
|
|
3
3
|
export const State = {
|
|
4
|
-
None: 'none'
|
|
5
|
-
Ready: 'ready'
|
|
6
|
-
Playing: 'playing'
|
|
7
|
-
Paused: 'paused'
|
|
8
|
-
Stopped: 'stopped'
|
|
9
|
-
Loading: 'loading'
|
|
10
|
-
Buffering: 'buffering'
|
|
11
|
-
Error: 'error'
|
|
12
|
-
Ended: 'ended'
|
|
4
|
+
None: 'none',
|
|
5
|
+
Ready: 'ready',
|
|
6
|
+
Playing: 'playing',
|
|
7
|
+
Paused: 'paused',
|
|
8
|
+
Stopped: 'stopped',
|
|
9
|
+
Loading: 'loading',
|
|
10
|
+
Buffering: 'buffering',
|
|
11
|
+
Error: 'error',
|
|
12
|
+
Ended: 'ended'
|
|
13
13
|
} satisfies Record<string, PlaybackState>
|
|
14
14
|
|
|
15
15
|
export type State = (typeof State)[keyof typeof State]
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import type { NavigationErrorType } from '../../features'
|
|
1
2
|
import type { Track, ResolvedTrack } from '../../types'
|
|
2
3
|
import type { NativeBrowserConfiguration } from '../../types/browser-native'
|
|
3
|
-
import type { NavigationErrorType } from '../../features'
|
|
4
|
-
import { SimpleRouter } from '../SimpleRouter'
|
|
5
4
|
import type { HttpClient } from '../http/HttpClient'
|
|
6
5
|
import type { FavoriteManager } from './FavoriteManager'
|
|
7
6
|
import type { NavigationErrorManager } from './NavigationErrorManager'
|
|
7
|
+
import { assertedNotNullish } from '../../utils/validation'
|
|
8
8
|
import { RequestConfigBuilder } from '../http/RequestConfigBuilder'
|
|
9
|
+
import { SimpleRouter } from '../SimpleRouter'
|
|
9
10
|
import { BrowserPathHelper } from '../util/BrowserPathHelper'
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -49,10 +50,23 @@ export class BrowserManager {
|
|
|
49
50
|
'message' in error &&
|
|
50
51
|
typeof error.message === 'string'
|
|
51
52
|
) {
|
|
52
|
-
const navError = error as {
|
|
53
|
-
|
|
53
|
+
const navError = error as {
|
|
54
|
+
code: NavigationErrorType
|
|
55
|
+
message: string
|
|
56
|
+
statusCode?: number
|
|
57
|
+
}
|
|
58
|
+
this.navigationErrorManager.setNavigationError(
|
|
59
|
+
navError.code,
|
|
60
|
+
navError.message,
|
|
61
|
+
path,
|
|
62
|
+
navError.statusCode
|
|
63
|
+
)
|
|
54
64
|
} else {
|
|
55
|
-
this.navigationErrorManager.setNavigationError(
|
|
65
|
+
this.navigationErrorManager.setNavigationError(
|
|
66
|
+
'network-error',
|
|
67
|
+
'Failed to load content',
|
|
68
|
+
path
|
|
69
|
+
)
|
|
56
70
|
}
|
|
57
71
|
}
|
|
58
72
|
|
|
@@ -69,7 +83,8 @@ export class BrowserManager {
|
|
|
69
83
|
set path(value: string | undefined) {
|
|
70
84
|
if (this.hasValidConfiguration()) {
|
|
71
85
|
this.navigationErrorManager.clearNavigationError()
|
|
72
|
-
const pathToNavigate =
|
|
86
|
+
const pathToNavigate =
|
|
87
|
+
value ?? this._configuration.path ?? this.getDefaultPath()
|
|
73
88
|
if (pathToNavigate) {
|
|
74
89
|
void this.navigate(pathToNavigate)
|
|
75
90
|
}
|
|
@@ -130,7 +145,7 @@ export class BrowserManager {
|
|
|
130
145
|
|
|
131
146
|
// If no path specified, try to get first tab URL
|
|
132
147
|
if (!initialPath) {
|
|
133
|
-
const tabsRoute = value.routes?.find(r => r.path === '__tabs__')
|
|
148
|
+
const tabsRoute = value.routes?.find((r) => r.path === '__tabs__')
|
|
134
149
|
if (tabsRoute?.browseStatic?.children?.[0]?.url) {
|
|
135
150
|
initialPath = tabsRoute.browseStatic.children[0].url
|
|
136
151
|
} else {
|
|
@@ -205,19 +220,14 @@ export class BrowserManager {
|
|
|
205
220
|
return
|
|
206
221
|
}
|
|
207
222
|
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
// reach this block because it returns directly above. This matches Android's
|
|
213
|
-
// architecture where search() bypasses the resolve() contextual URL logic.
|
|
214
|
-
// IMPORTANT: Create a shallow copy to avoid mutating the original config object
|
|
215
|
-
// (e.g., browseStatic from routes). Without this, static route children would
|
|
216
|
-
// accumulate contextual URLs, breaking search which reads from the same source.
|
|
223
|
+
// Add contextual URLs to non-search content (matches Android behavior where
|
|
224
|
+
// search() bypasses the resolve() contextual URL logic).
|
|
225
|
+
// Shallow copy to avoid mutating the original config object (e.g., browseStatic
|
|
226
|
+
// from routes), which would break search that reads from the same source.
|
|
217
227
|
if (content?.children && !isSearchPath) {
|
|
218
228
|
content = {
|
|
219
229
|
...content,
|
|
220
|
-
children: content.children.map(track => {
|
|
230
|
+
children: content.children.map((track) => {
|
|
221
231
|
// If track has src, always add/update contextual URL with current path context
|
|
222
232
|
// This matches Android's BrowserManager.kt:436-441
|
|
223
233
|
if (track.src) {
|
|
@@ -249,7 +259,9 @@ export class BrowserManager {
|
|
|
249
259
|
this.onContentChanged(content)
|
|
250
260
|
|
|
251
261
|
// Query and update tabs if configuration has __tabs__ route
|
|
252
|
-
const tabsRoute = this._configuration.routes?.find(
|
|
262
|
+
const tabsRoute = this._configuration.routes?.find(
|
|
263
|
+
(r) => r.path === '__tabs__'
|
|
264
|
+
)
|
|
253
265
|
if (tabsRoute) {
|
|
254
266
|
const tabs = await this.queryTabs()
|
|
255
267
|
|
|
@@ -271,9 +283,7 @@ export class BrowserManager {
|
|
|
271
283
|
this._content = undefined
|
|
272
284
|
this.onContentChanged(undefined)
|
|
273
285
|
|
|
274
|
-
|
|
275
|
-
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
276
|
-
this.navigationErrorManager.setNavigationError('unknown-error', message)
|
|
286
|
+
this.handleNavigationError(error, path)
|
|
277
287
|
}
|
|
278
288
|
}
|
|
279
289
|
|
|
@@ -285,7 +295,9 @@ export class BrowserManager {
|
|
|
285
295
|
* @param searchPath The search path (format: /__search?q=query)
|
|
286
296
|
* @returns ResolvedTrack containing search results as children
|
|
287
297
|
*/
|
|
288
|
-
private async resolveSearchContent(
|
|
298
|
+
private async resolveSearchContent(
|
|
299
|
+
searchPath: string
|
|
300
|
+
): Promise<ResolvedTrack | undefined> {
|
|
289
301
|
// Extract query from search path
|
|
290
302
|
const queryMatch = searchPath.match(/[?&]q=([^&]*)/)
|
|
291
303
|
if (!queryMatch) {
|
|
@@ -295,7 +307,9 @@ export class BrowserManager {
|
|
|
295
307
|
const query = decodeURIComponent(queryMatch[1] ?? '')
|
|
296
308
|
|
|
297
309
|
// Find __search__ route entry
|
|
298
|
-
const searchRoute = this._configuration.routes?.find(
|
|
310
|
+
const searchRoute = this._configuration.routes?.find(
|
|
311
|
+
(r) => r.path === '__search__'
|
|
312
|
+
)
|
|
299
313
|
if (!searchRoute) {
|
|
300
314
|
console.warn('No __search__ route configured')
|
|
301
315
|
return undefined
|
|
@@ -310,9 +324,12 @@ export class BrowserManager {
|
|
|
310
324
|
// Handle request config-based search
|
|
311
325
|
else if (searchRoute.searchConfig) {
|
|
312
326
|
const searchQueryParams: Record<string, string> = { q: query }
|
|
313
|
-
const requestConfig = this.httpClient.mergeRequestConfig(
|
|
314
|
-
|
|
315
|
-
|
|
327
|
+
const requestConfig = this.httpClient.mergeRequestConfig(
|
|
328
|
+
searchRoute.searchConfig,
|
|
329
|
+
{
|
|
330
|
+
query: searchQueryParams
|
|
331
|
+
}
|
|
332
|
+
)
|
|
316
333
|
|
|
317
334
|
try {
|
|
318
335
|
const response = await this.httpClient.executeRequest(requestConfig)
|
|
@@ -328,7 +345,7 @@ export class BrowserManager {
|
|
|
328
345
|
return {
|
|
329
346
|
url: searchPath,
|
|
330
347
|
title: `Search: ${query}`,
|
|
331
|
-
children: searchResults
|
|
348
|
+
children: searchResults
|
|
332
349
|
}
|
|
333
350
|
}
|
|
334
351
|
|
|
@@ -337,27 +354,31 @@ export class BrowserManager {
|
|
|
337
354
|
* Supports both static config and resolve/transform callbacks.
|
|
338
355
|
* Matches Android's artwork URL transformation with full Track access.
|
|
339
356
|
*/
|
|
340
|
-
private async transformArtworkForContent(
|
|
357
|
+
private async transformArtworkForContent(
|
|
358
|
+
content: ResolvedTrack
|
|
359
|
+
): Promise<ResolvedTrack> {
|
|
341
360
|
const artworkConfig = this._configuration.artwork
|
|
342
361
|
if (!artworkConfig) {
|
|
343
362
|
return content
|
|
344
363
|
}
|
|
345
364
|
|
|
346
365
|
// Transform parent artwork
|
|
347
|
-
const parentArtworkSource =
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
366
|
+
const parentArtworkSource =
|
|
367
|
+
await RequestConfigBuilder.resolveArtworkSourceAsync(
|
|
368
|
+
content,
|
|
369
|
+
artworkConfig
|
|
370
|
+
)
|
|
351
371
|
|
|
352
372
|
// Transform children artwork
|
|
353
373
|
let transformedChildren: Track[] | undefined
|
|
354
374
|
if (content.children) {
|
|
355
375
|
transformedChildren = await Promise.all(
|
|
356
|
-
content.children.map(async track => {
|
|
357
|
-
const artworkSource =
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
376
|
+
content.children.map(async (track) => {
|
|
377
|
+
const artworkSource =
|
|
378
|
+
await RequestConfigBuilder.resolveArtworkSourceAsync(
|
|
379
|
+
track,
|
|
380
|
+
artworkConfig
|
|
381
|
+
)
|
|
361
382
|
if (artworkSource && !track.artworkSource) {
|
|
362
383
|
return { ...track, artworkSource }
|
|
363
384
|
}
|
|
@@ -369,7 +390,7 @@ export class BrowserManager {
|
|
|
369
390
|
return {
|
|
370
391
|
...content,
|
|
371
392
|
artworkSource: parentArtworkSource ?? content.artworkSource,
|
|
372
|
-
children: transformedChildren ?? content.children
|
|
393
|
+
children: transformedChildren ?? content.children
|
|
373
394
|
}
|
|
374
395
|
}
|
|
375
396
|
|
|
@@ -385,12 +406,20 @@ export class BrowserManager {
|
|
|
385
406
|
*/
|
|
386
407
|
private async resolveRouteContent(
|
|
387
408
|
route: {
|
|
388
|
-
browseCallback?: NativeBrowserConfiguration['routes'] extends
|
|
389
|
-
|
|
409
|
+
browseCallback?: NativeBrowserConfiguration['routes'] extends
|
|
410
|
+
| (infer R)[]
|
|
411
|
+
| undefined
|
|
412
|
+
? R extends { browseCallback?: infer C }
|
|
413
|
+
? C
|
|
414
|
+
: never
|
|
390
415
|
: never
|
|
391
416
|
browseStatic?: ResolvedTrack
|
|
392
|
-
browseConfig?: NativeBrowserConfiguration['routes'] extends
|
|
393
|
-
|
|
417
|
+
browseConfig?: NativeBrowserConfiguration['routes'] extends
|
|
418
|
+
| (infer R)[]
|
|
419
|
+
| undefined
|
|
420
|
+
? R extends { browseConfig?: infer C }
|
|
421
|
+
? C
|
|
422
|
+
: never
|
|
394
423
|
: never
|
|
395
424
|
},
|
|
396
425
|
path: string,
|
|
@@ -419,7 +448,10 @@ export class BrowserManager {
|
|
|
419
448
|
|
|
420
449
|
// Handle request config-based route
|
|
421
450
|
if (route.browseConfig) {
|
|
422
|
-
const requestConfig = this.httpClient.mergeRequestConfig(
|
|
451
|
+
const requestConfig = this.httpClient.mergeRequestConfig(
|
|
452
|
+
route.browseConfig,
|
|
453
|
+
{ path }
|
|
454
|
+
)
|
|
423
455
|
try {
|
|
424
456
|
const response = await this.httpClient.executeRequest(requestConfig)
|
|
425
457
|
return response as ResolvedTrack
|
|
@@ -436,18 +468,23 @@ export class BrowserManager {
|
|
|
436
468
|
/**
|
|
437
469
|
* Resolves content for a specific path using configured routes.
|
|
438
470
|
*/
|
|
439
|
-
private async resolveContent(
|
|
471
|
+
private async resolveContent(
|
|
472
|
+
path: string
|
|
473
|
+
): Promise<ResolvedTrack | undefined> {
|
|
440
474
|
const routes = this._configuration.routes
|
|
441
475
|
if (!routes || routes.length === 0) {
|
|
442
476
|
return undefined
|
|
443
477
|
}
|
|
444
478
|
|
|
445
479
|
// Convert routes array to record for SimpleRouter
|
|
446
|
-
const routePatterns: Record<
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
480
|
+
const routePatterns: Record<
|
|
481
|
+
string,
|
|
482
|
+
{
|
|
483
|
+
browseCallback?: (typeof routes)[0]['browseCallback']
|
|
484
|
+
browseConfig?: (typeof routes)[0]['browseConfig']
|
|
485
|
+
browseStatic?: (typeof routes)[0]['browseStatic']
|
|
486
|
+
}
|
|
487
|
+
> = {}
|
|
451
488
|
|
|
452
489
|
for (const route of routes) {
|
|
453
490
|
// Skip special routes
|
|
@@ -456,7 +493,7 @@ export class BrowserManager {
|
|
|
456
493
|
routePatterns[route.path] = {
|
|
457
494
|
browseCallback: route.browseCallback,
|
|
458
495
|
browseConfig: route.browseConfig,
|
|
459
|
-
browseStatic: route.browseStatic
|
|
496
|
+
browseStatic: route.browseStatic
|
|
460
497
|
}
|
|
461
498
|
}
|
|
462
499
|
|
|
@@ -464,14 +501,19 @@ export class BrowserManager {
|
|
|
464
501
|
const match = this.router.findBestMatch(path, routePatterns)
|
|
465
502
|
if (match) {
|
|
466
503
|
const [matchedPattern, routeMatch] = match
|
|
467
|
-
const matchedRoute = routes.find(r => r.path === matchedPattern)
|
|
504
|
+
const matchedRoute = routes.find((r) => r.path === matchedPattern)
|
|
468
505
|
if (matchedRoute) {
|
|
469
|
-
return this.resolveRouteContent(
|
|
506
|
+
return this.resolveRouteContent(
|
|
507
|
+
matchedRoute,
|
|
508
|
+
path,
|
|
509
|
+
routeMatch.params,
|
|
510
|
+
'Route'
|
|
511
|
+
)
|
|
470
512
|
}
|
|
471
513
|
}
|
|
472
514
|
|
|
473
515
|
// Fall back to __default__ route
|
|
474
|
-
const defaultRoute = routes.find(r => r.path === '__default__')
|
|
516
|
+
const defaultRoute = routes.find((r) => r.path === '__default__')
|
|
475
517
|
if (defaultRoute) {
|
|
476
518
|
return this.resolveRouteContent(defaultRoute, path, {}, 'Default route')
|
|
477
519
|
}
|
|
@@ -483,7 +525,9 @@ export class BrowserManager {
|
|
|
483
525
|
* Queries tabs from the __tabs__ route.
|
|
484
526
|
*/
|
|
485
527
|
private async queryTabs(): Promise<Track[]> {
|
|
486
|
-
const tabsRoute = this._configuration.routes?.find(
|
|
528
|
+
const tabsRoute = this._configuration.routes?.find(
|
|
529
|
+
(r) => r.path === '__tabs__'
|
|
530
|
+
)
|
|
487
531
|
if (!tabsRoute) {
|
|
488
532
|
return []
|
|
489
533
|
}
|
|
@@ -492,14 +536,20 @@ export class BrowserManager {
|
|
|
492
536
|
const tabs = result?.children ?? []
|
|
493
537
|
|
|
494
538
|
// Transform artwork URLs on tabs
|
|
495
|
-
return RequestConfigBuilder.transformTracksArtwork(
|
|
539
|
+
return RequestConfigBuilder.transformTracksArtwork(
|
|
540
|
+
tabs,
|
|
541
|
+
this._configuration.artwork
|
|
542
|
+
)
|
|
496
543
|
}
|
|
497
544
|
|
|
498
545
|
/**
|
|
499
546
|
* Checks if the configuration has valid routes.
|
|
500
547
|
*/
|
|
501
548
|
private hasValidConfiguration(): boolean {
|
|
502
|
-
return
|
|
549
|
+
return (
|
|
550
|
+
this._configuration.routes !== undefined &&
|
|
551
|
+
this._configuration.routes.length > 0
|
|
552
|
+
)
|
|
503
553
|
}
|
|
504
554
|
|
|
505
555
|
/**
|
|
@@ -528,7 +578,7 @@ export class BrowserManager {
|
|
|
528
578
|
}
|
|
529
579
|
|
|
530
580
|
// Filter to only playable tracks (tracks with src)
|
|
531
|
-
const playableTracks = children.filter(track => track.src != null)
|
|
581
|
+
const playableTracks = children.filter((track) => track.src != null)
|
|
532
582
|
|
|
533
583
|
if (playableTracks.length === 0) {
|
|
534
584
|
console.warn('Parent has no playable tracks, cannot expand queue')
|
|
@@ -536,23 +586,30 @@ export class BrowserManager {
|
|
|
536
586
|
}
|
|
537
587
|
|
|
538
588
|
// Find the index of the selected track in the playable tracks array
|
|
539
|
-
const selectedIndex = playableTracks.findIndex(
|
|
589
|
+
const selectedIndex = playableTracks.findIndex(
|
|
590
|
+
(track) => track.src === trackId
|
|
591
|
+
)
|
|
540
592
|
|
|
541
593
|
if (selectedIndex < 0) {
|
|
542
|
-
console.warn(
|
|
594
|
+
console.warn(
|
|
595
|
+
`Track with src='${trackId}' not found in playable children`
|
|
596
|
+
)
|
|
543
597
|
return undefined
|
|
544
598
|
}
|
|
545
599
|
|
|
546
600
|
// Check singleTrack setting - if true, return only the selected track
|
|
547
601
|
if (this._configuration.singleTrack) {
|
|
548
602
|
return {
|
|
549
|
-
tracks: [playableTracks[selectedIndex]
|
|
550
|
-
selectedIndex: 0
|
|
603
|
+
tracks: [assertedNotNullish(playableTracks[selectedIndex])],
|
|
604
|
+
selectedIndex: 0
|
|
551
605
|
}
|
|
552
606
|
}
|
|
553
607
|
return { tracks: playableTracks, selectedIndex }
|
|
554
608
|
} catch (error) {
|
|
555
|
-
console.error(
|
|
609
|
+
console.error(
|
|
610
|
+
`Error expanding queue from contextual URL: ${contextualUrl}`,
|
|
611
|
+
error
|
|
612
|
+
)
|
|
556
613
|
return undefined
|
|
557
614
|
}
|
|
558
615
|
}
|
|
@@ -576,13 +633,13 @@ export class BrowserManager {
|
|
|
576
633
|
): Promise<{ tracks: Track[]; startIndex: number; startPositionMs: number }> {
|
|
577
634
|
// Single track: check for search context or contextual URL
|
|
578
635
|
if (tracks.length === 1) {
|
|
579
|
-
const track = tracks[0]
|
|
636
|
+
const track = assertedNotNullish(tracks[0])
|
|
580
637
|
const trackUrl = track.url
|
|
581
638
|
|
|
582
639
|
// If search query present, expand search results
|
|
583
640
|
if (searchQuery) {
|
|
584
641
|
// Execute search (will hit cache if already performed)
|
|
585
|
-
const searchResults = await this.
|
|
642
|
+
const searchResults = await this.resolveSearchContent(
|
|
586
643
|
BrowserPathHelper.createSearchPath(searchQuery)
|
|
587
644
|
)
|
|
588
645
|
const searchTracks = searchResults?.children
|
|
@@ -590,14 +647,14 @@ export class BrowserManager {
|
|
|
590
647
|
if (searchTracks && searchTracks.length > 0) {
|
|
591
648
|
// Find the selected track in search results
|
|
592
649
|
const selectedIdx = searchTracks.findIndex(
|
|
593
|
-
t => t.url === trackUrl || t.src === track.src
|
|
650
|
+
(t) => t.url === trackUrl || t.src === track.src
|
|
594
651
|
)
|
|
595
652
|
|
|
596
653
|
if (selectedIdx >= 0) {
|
|
597
654
|
return {
|
|
598
655
|
tracks: searchTracks,
|
|
599
656
|
startIndex: selectedIdx,
|
|
600
|
-
startPositionMs
|
|
657
|
+
startPositionMs
|
|
601
658
|
}
|
|
602
659
|
}
|
|
603
660
|
}
|
|
@@ -618,7 +675,7 @@ export class BrowserManager {
|
|
|
618
675
|
return {
|
|
619
676
|
tracks: expanded.tracks,
|
|
620
677
|
startIndex: expanded.selectedIndex,
|
|
621
|
-
startPositionMs
|
|
678
|
+
startPositionMs
|
|
622
679
|
}
|
|
623
680
|
}
|
|
624
681
|
}
|