@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.
Files changed (150) hide show
  1. package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +11 -71
  2. package/android/src/main/java/com/audiobrowser/player/TransformingDataSource.kt +125 -0
  3. package/ios/Browser/BrowserManager.swift +17 -2
  4. package/ios/TrackPlayer.swift +145 -31
  5. package/lib/commonjs/features/browser.js.map +1 -1
  6. package/lib/commonjs/features/equalizer.js.map +1 -1
  7. package/lib/commonjs/features/errors.js.map +1 -1
  8. package/lib/commonjs/features/favorites.js.map +1 -1
  9. package/lib/commonjs/features/metadata.js.map +1 -1
  10. package/lib/commonjs/features/nowPlaying.js.map +1 -1
  11. package/lib/commonjs/features/output.js.map +1 -1
  12. package/lib/commonjs/features/playback/state.js.map +1 -1
  13. package/lib/commonjs/features/player/options.js.map +1 -1
  14. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  15. package/lib/commonjs/features/queue/queue.js +1 -1
  16. package/lib/commonjs/features/queue/queue.js.map +1 -1
  17. package/lib/commonjs/features/remoteControls.js.map +1 -1
  18. package/lib/commonjs/utils/useDebug.js.map +1 -1
  19. package/lib/commonjs/utils/validation.js +23 -0
  20. package/lib/commonjs/utils/validation.js.map +1 -0
  21. package/lib/commonjs/web/NativeAudioBrowser.js +53 -15
  22. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  23. package/lib/commonjs/web/SimpleRouter.js +5 -4
  24. package/lib/commonjs/web/SimpleRouter.js.map +1 -1
  25. package/lib/commonjs/web/TrackPlayer/Player.js +49 -24
  26. package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
  27. package/lib/commonjs/web/TrackPlayer/PlaylistPlayer.js +54 -45
  28. package/lib/commonjs/web/TrackPlayer/PlaylistPlayer.js.map +1 -1
  29. package/lib/commonjs/web/TrackPlayer/State.js.map +1 -1
  30. package/lib/commonjs/web/browser/BrowserManager.js +10 -17
  31. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  32. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  33. package/lib/commonjs/web/browser/NavigationErrorManager.js.map +1 -1
  34. package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
  35. package/lib/commonjs/web/http/HttpClient.js +15 -2
  36. package/lib/commonjs/web/http/HttpClient.js.map +1 -1
  37. package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
  38. package/lib/commonjs/web/player/NowPlayingManager.js +9 -0
  39. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  40. package/lib/commonjs/web/player/OptionsManager.js +1 -1
  41. package/lib/commonjs/web/player/OptionsManager.js.map +1 -1
  42. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  43. package/lib/module/features/browser.js.map +1 -1
  44. package/lib/module/features/equalizer.js.map +1 -1
  45. package/lib/module/features/errors.js.map +1 -1
  46. package/lib/module/features/favorites.js.map +1 -1
  47. package/lib/module/features/metadata.js.map +1 -1
  48. package/lib/module/features/nowPlaying.js +1 -0
  49. package/lib/module/features/nowPlaying.js.map +1 -1
  50. package/lib/module/features/output.js.map +1 -1
  51. package/lib/module/features/playback/state.js.map +1 -1
  52. package/lib/module/features/player/options.js.map +1 -1
  53. package/lib/module/features/queue/activeTrack.js.map +1 -1
  54. package/lib/module/features/queue/queue.js +1 -1
  55. package/lib/module/features/queue/queue.js.map +1 -1
  56. package/lib/module/features/remoteControls.js.map +1 -1
  57. package/lib/module/utils/useDebug.js.map +1 -1
  58. package/lib/module/utils/validation.js +18 -0
  59. package/lib/module/utils/validation.js.map +1 -0
  60. package/lib/module/web/NativeAudioBrowser.js +53 -15
  61. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  62. package/lib/module/web/SimpleRouter.js +5 -4
  63. package/lib/module/web/SimpleRouter.js.map +1 -1
  64. package/lib/module/web/TrackPlayer/Player.js +49 -24
  65. package/lib/module/web/TrackPlayer/Player.js.map +1 -1
  66. package/lib/module/web/TrackPlayer/PlaylistPlayer.js +54 -45
  67. package/lib/module/web/TrackPlayer/PlaylistPlayer.js.map +1 -1
  68. package/lib/module/web/TrackPlayer/State.js.map +1 -1
  69. package/lib/module/web/browser/BrowserManager.js +10 -17
  70. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  71. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  72. package/lib/module/web/browser/NavigationErrorManager.js.map +1 -1
  73. package/lib/module/web/browser/SearchManager.js.map +1 -1
  74. package/lib/module/web/http/HttpClient.js +15 -2
  75. package/lib/module/web/http/HttpClient.js.map +1 -1
  76. package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
  77. package/lib/module/web/player/NowPlayingManager.js +9 -0
  78. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  79. package/lib/module/web/player/OptionsManager.js +1 -1
  80. package/lib/module/web/player/OptionsManager.js.map +1 -1
  81. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  82. package/lib/typescript/src/features/browser.d.ts.map +1 -1
  83. package/lib/typescript/src/features/equalizer.d.ts.map +1 -1
  84. package/lib/typescript/src/features/errors.d.ts.map +1 -1
  85. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  86. package/lib/typescript/src/features/metadata.d.ts +1 -1
  87. package/lib/typescript/src/features/metadata.d.ts.map +1 -1
  88. package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
  89. package/lib/typescript/src/features/nowPlaying.d.ts.map +1 -1
  90. package/lib/typescript/src/features/output.d.ts.map +1 -1
  91. package/lib/typescript/src/features/playback/state.d.ts +1 -1
  92. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  93. package/lib/typescript/src/features/player/options.d.ts +1 -1
  94. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  95. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  96. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  97. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  98. package/lib/typescript/src/specs/audio-browser.nitro.d.ts.map +1 -1
  99. package/lib/typescript/src/utils/useDebug.d.ts +1 -1
  100. package/lib/typescript/src/utils/useDebug.d.ts.map +1 -1
  101. package/lib/typescript/src/utils/validation.d.ts +3 -0
  102. package/lib/typescript/src/utils/validation.d.ts.map +1 -0
  103. package/lib/typescript/src/web/NativeAudioBrowser.d.ts +6 -1
  104. package/lib/typescript/src/web/NativeAudioBrowser.d.ts.map +1 -1
  105. package/lib/typescript/src/web/SimpleRouter.d.ts.map +1 -1
  106. package/lib/typescript/src/web/TrackPlayer/Player.d.ts +7 -19
  107. package/lib/typescript/src/web/TrackPlayer/Player.d.ts.map +1 -1
  108. package/lib/typescript/src/web/TrackPlayer/PlaylistPlayer.d.ts +3 -3
  109. package/lib/typescript/src/web/TrackPlayer/PlaylistPlayer.d.ts.map +1 -1
  110. package/lib/typescript/src/web/browser/BrowserManager.d.ts.map +1 -1
  111. package/lib/typescript/src/web/browser/FavoriteManager.d.ts.map +1 -1
  112. package/lib/typescript/src/web/browser/NavigationErrorManager.d.ts.map +1 -1
  113. package/lib/typescript/src/web/browser/SearchManager.d.ts +1 -1
  114. package/lib/typescript/src/web/browser/SearchManager.d.ts.map +1 -1
  115. package/lib/typescript/src/web/http/HttpClient.d.ts.map +1 -1
  116. package/lib/typescript/src/web/http/RequestConfigBuilder.d.ts.map +1 -1
  117. package/lib/typescript/src/web/player/NowPlayingManager.d.ts.map +1 -1
  118. package/lib/typescript/src/web/player/OptionsManager.d.ts.map +1 -1
  119. package/lib/typescript/src/web/util/BrowserPathHelper.d.ts.map +1 -1
  120. package/package.json +1 -1
  121. package/src/features/browser.ts +1 -1
  122. package/src/features/equalizer.ts +1 -1
  123. package/src/features/errors.ts +1 -1
  124. package/src/features/favorites.ts +1 -1
  125. package/src/features/metadata.ts +1 -1
  126. package/src/features/nowPlaying.ts +1 -1
  127. package/src/features/output.ts +1 -1
  128. package/src/features/playback/state.ts +1 -1
  129. package/src/features/player/options.ts +2 -2
  130. package/src/features/queue/activeTrack.ts +1 -1
  131. package/src/features/queue/queue.ts +2 -2
  132. package/src/features/remoteControls.ts +2 -2
  133. package/src/specs/audio-browser.nitro.ts +0 -1
  134. package/src/utils/useDebug.ts +6 -6
  135. package/src/utils/validation.ts +27 -0
  136. package/src/web/NativeAudioBrowser.ts +137 -58
  137. package/src/web/SimpleRouter.ts +24 -9
  138. package/src/web/TrackPlayer/Player.ts +58 -30
  139. package/src/web/TrackPlayer/PlaylistPlayer.ts +72 -63
  140. package/src/web/TrackPlayer/RepeatMode.ts +1 -1
  141. package/src/web/TrackPlayer/State.ts +9 -9
  142. package/src/web/browser/BrowserManager.ts +124 -67
  143. package/src/web/browser/FavoriteManager.ts +5 -3
  144. package/src/web/browser/NavigationErrorManager.ts +15 -8
  145. package/src/web/browser/SearchManager.ts +17 -11
  146. package/src/web/http/HttpClient.ts +25 -7
  147. package/src/web/http/RequestConfigBuilder.ts +29 -13
  148. package/src/web/player/NowPlayingManager.ts +13 -7
  149. package/src/web/player/OptionsManager.ts +7 -6
  150. 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 { RepeatMode } from './RepeatMode'
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 onCompletedLoading = (_track: Track) => {
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, onCompletedLoading)
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 = this.getPreviousIndex() ?? this.getWrapAroundLastIndex()
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) return this.shuffleOrder[this.shuffleOrder.length - 1]
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({ length: this.playlist.length }, (_, i) => i)
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 idxMap = indexes.reduce<Record<number, boolean>>((acc, elem) => {
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 = !idxMap[idx]
247
+ const keep = !idxSet.has(idx)
246
248
 
247
- if (!keep && idx === this.currentIndex) {
248
- isCurrentRemoved = true
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
- const hasItems = this.playlist.length > 0
259
- if (isCurrentRemoved && hasItems) {
260
- this.goToIndex(this.currentIndex % this.playlist.length)
261
- } else if (isCurrentRemoved) {
262
- this.stop()
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
- this.stop(() => {
280
- this.playlist = []
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
- if (this.currentIndex === fromIndex) {
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
- if (this.currentIndex !== undefined && shift) {
329
- this.currentIndex = this.currentIndex + shift
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
@@ -3,5 +3,5 @@ import type { RepeatMode as RepeatModeType } from '../../features'
3
3
  export const RepeatMode = {
4
4
  Off: 'off' as const,
5
5
  Track: 'track' as const,
6
- Playlist: 'queue' as const,
6
+ Playlist: 'queue' as const
7
7
  } satisfies Record<string, RepeatModeType>
@@ -1,15 +1,15 @@
1
1
  import type { PlaybackState } from '../../features'
2
2
 
3
3
  export const State = {
4
- None: 'none' as const,
5
- Ready: 'ready' as const,
6
- Playing: 'playing' as const,
7
- Paused: 'paused' as const,
8
- Stopped: 'stopped' as const,
9
- Loading: 'loading' as const,
10
- Buffering: 'buffering' as const,
11
- Error: 'error' as const,
12
- Ended: 'ended' as const
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 { code: NavigationErrorType; message: string; statusCode?: number }
53
- this.navigationErrorManager.setNavigationError(navError.code, navError.message, path, navError.statusCode)
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('network-error', 'Failed to load content', path)
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 = value ?? this._configuration.path ?? this.getDefaultPath()
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
- // Transform tracks with src to add contextual URLs (matches Android behavior)
209
- // Android ALWAYS regenerates contextual URLs for tracks with src to reflect
210
- // current browsing context, enabling proper queue expansion.
211
- // Note: Search content is resolved via resolveSearchContent() which doesn't
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(r => r.path === '__tabs__')
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
- // Set navigation error using helper
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(searchPath: string): Promise<ResolvedTrack | undefined> {
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(r => r.path === '__search__')
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(searchRoute.searchConfig, {
314
- query: searchQueryParams
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(content: ResolvedTrack): Promise<ResolvedTrack> {
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 = await RequestConfigBuilder.resolveArtworkSourceAsync(
348
- content,
349
- artworkConfig
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 = await RequestConfigBuilder.resolveArtworkSourceAsync(
358
- track,
359
- artworkConfig
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 (infer R)[] | undefined
389
- ? R extends { browseCallback?: infer C } ? C : never
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 (infer R)[] | undefined
393
- ? R extends { browseConfig?: infer C } ? C : never
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(route.browseConfig, { path })
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(path: string): Promise<ResolvedTrack | undefined> {
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<string, {
447
- browseCallback?: typeof routes[0]['browseCallback']
448
- browseConfig?: typeof routes[0]['browseConfig']
449
- browseStatic?: typeof routes[0]['browseStatic']
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(matchedRoute, path, routeMatch.params, 'Route')
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(r => r.path === '__tabs__')
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(tabs, this._configuration.artwork)
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 this._configuration.routes !== undefined && this._configuration.routes.length > 0
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(track => track.src === trackId)
589
+ const selectedIndex = playableTracks.findIndex(
590
+ (track) => track.src === trackId
591
+ )
540
592
 
541
593
  if (selectedIndex < 0) {
542
- console.warn(`Track with src='${trackId}' not found in playable children`)
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(`Error expanding queue from contextual URL: ${contextualUrl}`, 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.resolveContent(
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
  }