react-native-audio-browser 0.2.0-next.14 → 0.2.0-next.16
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/AudioBrowser.kt +30 -10
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +3 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +4 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +1 -0
- package/android/src/main/java/com/audiobrowser/model/IcyTitle.kt +24 -0
- package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +10 -6
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +2 -0
- package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +1 -1
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +1 -0
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +3 -0
- package/android/src/test/java/com/audiobrowser/model/IcyTitleTest.kt +49 -0
- package/ios/Browser/BrowserManager.swift +1 -0
- package/ios/Browser/JsonModels.swift +11 -0
- package/ios/CarPlay/CarPlayImageLoader.swift +4 -2
- package/ios/CarPlay/CarPlayListItemFactory.swift +4 -4
- package/ios/Extension/ResolvedTrack+Copying.swift +2 -0
- package/ios/Extension/String+IcyCharset.swift +21 -0
- package/ios/Extension/TimedMetadata+AVFoundation.swift +6 -4
- package/ios/Extension/Track+Copying.swift +2 -0
- package/ios/HybridAudioBrowser.swift +1 -1
- package/ios/Model/NitroTypeStubs.swift +2 -0
- package/ios/Player/SeekCompletionHandler.swift +3 -1
- package/ios/Tests/PlaybackStateStoreTests.swift +1 -1
- package/ios/Tests/ResolvedTrackCopyingTests.swift +2 -0
- package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
- package/lib/typescript/src/types/browser-nodes.d.ts +14 -0
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +7 -3
- package/nitrogen/generated/android/c++/JTrack.hpp +7 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -3
- package/nitrogen/generated/ios/swift/ResolvedTrack.swift +19 -1
- package/nitrogen/generated/ios/swift/Track.swift +19 -1
- package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +6 -2
- package/nitrogen/generated/shared/c++/Track.hpp +6 -2
- package/package.json +10 -10
- package/src/types/browser-nodes.ts +15 -0
- package/website/guide/track.md +1 -0
|
@@ -460,8 +460,14 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
460
460
|
browserManager.config = buildConfig()
|
|
461
461
|
|
|
462
462
|
// Notify player that browser configuration is ready (routes/tabs available)
|
|
463
|
-
// This allows Android Auto to start browsing content
|
|
464
|
-
|
|
463
|
+
// This allows Android Auto to start browsing content.
|
|
464
|
+
// This reaches MediaSession.getConnectedControllers, which asserts the
|
|
465
|
+
// main thread, and this setter runs on the JS thread. Hop without
|
|
466
|
+
// blocking -- runBlockingOnMain would deadlock here for the same reason
|
|
467
|
+
// described for the navigation below (JS callbacks can't run while the
|
|
468
|
+
// JS thread waits). Ordering with navigationJob is preserved: both are
|
|
469
|
+
// queued on mainScope.
|
|
470
|
+
launchInScope { connectedService?.player?.notifyBrowserConfigurationReady() }
|
|
465
471
|
|
|
466
472
|
clearNavigationError()
|
|
467
473
|
|
|
@@ -777,8 +783,9 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
777
783
|
// Invalidate cached content so future navigations fetch fresh data
|
|
778
784
|
browserManager.invalidateContentCache(path)
|
|
779
785
|
|
|
780
|
-
// Notify external media controllers (Android Auto)
|
|
781
|
-
|
|
786
|
+
// Notify external media controllers (Android Auto). Reaches MediaSession,
|
|
787
|
+
// which asserts the main thread; this override runs on the JS thread.
|
|
788
|
+
launchInScope { connectedService?.player?.notifyContentChanged(path) }
|
|
782
789
|
|
|
783
790
|
// If we're currently viewing this path, refresh the content
|
|
784
791
|
if (browserManager.getPath() == path) {
|
|
@@ -792,8 +799,10 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
792
799
|
// Clear all cached content so every path re-fetches fresh data
|
|
793
800
|
browserManager.clearContentCache()
|
|
794
801
|
|
|
795
|
-
// Notify external media controllers (Android Auto) to refresh subscribed paths
|
|
796
|
-
|
|
802
|
+
// Notify external media controllers (Android Auto) to refresh subscribed paths.
|
|
803
|
+
// Reaches MediaSession, which asserts the main thread; this override runs on
|
|
804
|
+
// the JS thread.
|
|
805
|
+
launchInScope { connectedService?.player?.invalidateAllContent() }
|
|
797
806
|
|
|
798
807
|
// Refresh whatever the browser is currently viewing
|
|
799
808
|
mainScope.launch { browserManager.refresh() }
|
|
@@ -834,14 +843,16 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
834
843
|
// Re-query every subscribed parent so a connected controller (Android
|
|
835
844
|
// Auto) swaps its lists for the gate tile without reconnecting. Notify
|
|
836
845
|
// only — the content cache stays warm for when the gate clears.
|
|
837
|
-
|
|
846
|
+
// MediaSession asserts the main thread; this override runs on the JS thread.
|
|
847
|
+
launchInScope { connectedService?.player?.invalidateAllContent() }
|
|
838
848
|
}
|
|
839
849
|
|
|
840
850
|
override fun clearGate() {
|
|
841
851
|
if (!gateState.active) return
|
|
842
852
|
// Single atomic assignment — readers never see a half-cleared triple.
|
|
843
853
|
gateState = GateState(active = false, chrome = null, hasResolver = false)
|
|
844
|
-
|
|
854
|
+
// MediaSession asserts the main thread; this override runs on the JS thread.
|
|
855
|
+
launchInScope { connectedService?.player?.invalidateAllContent() }
|
|
845
856
|
}
|
|
846
857
|
|
|
847
858
|
/**
|
|
@@ -927,8 +938,17 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
927
938
|
|
|
928
939
|
override fun updateOptions(options: NativeUpdateOptions) {
|
|
929
940
|
updateOptions.updateFromBridge(options)
|
|
930
|
-
// Only update the options if the service is around
|
|
931
|
-
|
|
941
|
+
// Only update the options if the service is around.
|
|
942
|
+
// applyOptions touches the Media3 player, which asserts it is only ever
|
|
943
|
+
// driven from the main thread ("Player callback method is called from a
|
|
944
|
+
// wrong thread"). Nitro invokes this from the JS thread, so hop.
|
|
945
|
+
//
|
|
946
|
+
// The hop blocks rather than launching because `updateOptions` is a single
|
|
947
|
+
// mutable holder: updateFromBridge mutates it in place on the JS thread and
|
|
948
|
+
// applyOptions reads it on main. Blocking is what keeps the two ordered.
|
|
949
|
+
connectedService?.let { service ->
|
|
950
|
+
runBlockingOnMain { service.player.applyOptions(updateOptions) }
|
|
951
|
+
}
|
|
932
952
|
}
|
|
933
953
|
|
|
934
954
|
override fun getOptions(): Options {
|
|
@@ -359,6 +359,7 @@ class BrowserManager {
|
|
|
359
359
|
disabled = null,
|
|
360
360
|
favorited = null,
|
|
361
361
|
live = null,
|
|
362
|
+
icyCharset = null,
|
|
362
363
|
)
|
|
363
364
|
)
|
|
364
365
|
}
|
|
@@ -1005,6 +1006,7 @@ class BrowserManager {
|
|
|
1005
1006
|
disabled = null,
|
|
1006
1007
|
favorited = null,
|
|
1007
1008
|
live = null,
|
|
1009
|
+
icyCharset = null,
|
|
1008
1010
|
)
|
|
1009
1011
|
|
|
1010
1012
|
// Cache search results for getCachedSearchResults()
|
|
@@ -1044,6 +1046,7 @@ class BrowserManager {
|
|
|
1044
1046
|
disabled = null,
|
|
1045
1047
|
favorited = null,
|
|
1046
1048
|
live = null,
|
|
1049
|
+
icyCharset = null,
|
|
1047
1050
|
)
|
|
1048
1051
|
|
|
1049
1052
|
return emptySearchResult
|
|
@@ -65,6 +65,7 @@ data class JsonResolvedTrack(
|
|
|
65
65
|
val style: JsonElement? = null,
|
|
66
66
|
val disabled: Boolean? = null,
|
|
67
67
|
val live: Boolean? = null,
|
|
68
|
+
val icyCharset: String? = null,
|
|
68
69
|
val carPlaySiriListButton: String? = null,
|
|
69
70
|
)
|
|
70
71
|
|
|
@@ -87,6 +88,7 @@ data class JsonTrack(
|
|
|
87
88
|
val style: JsonElement? = null,
|
|
88
89
|
val disabled: Boolean? = null,
|
|
89
90
|
val live: Boolean? = null,
|
|
91
|
+
val icyCharset: String? = null,
|
|
90
92
|
)
|
|
91
93
|
|
|
92
94
|
/**
|
|
@@ -179,6 +181,7 @@ fun JsonResolvedTrack.toNitro(): ResolvedTrack {
|
|
|
179
181
|
disabled = disabled,
|
|
180
182
|
favorited = null,
|
|
181
183
|
live = live,
|
|
184
|
+
icyCharset = icyCharset,
|
|
182
185
|
)
|
|
183
186
|
}
|
|
184
187
|
|
|
@@ -202,5 +205,6 @@ fun JsonTrack.toNitro(): Track {
|
|
|
202
205
|
disabled = disabled,
|
|
203
206
|
favorited = null,
|
|
204
207
|
live = live,
|
|
208
|
+
icyCharset = icyCharset,
|
|
205
209
|
)
|
|
206
210
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package com.audiobrowser.model
|
|
2
|
+
|
|
3
|
+
import java.nio.charset.Charset
|
|
4
|
+
|
|
5
|
+
/** Re-decodes an ICY `StreamTitle` with the charset the track declares. */
|
|
6
|
+
internal object IcyTitle {
|
|
7
|
+
/**
|
|
8
|
+
* [title] re-decoded through ISO-8859-1 with [charset] when it is Latin-1 shaped: every char at
|
|
9
|
+
* or below U+00FF, at least one at or above U+0080. That shape is what media3's ISO-8859-1
|
|
10
|
+
* fallback yields for bytes that are not UTF-8, and also what a station that UTF-8-encoded its
|
|
11
|
+
* legacy bytes yields; ISO-8859-1 is lossless, so both re-decode. Returns [title] as is when it
|
|
12
|
+
* has another shape, is pure ASCII, or [charset] is not one the runtime can decode with.
|
|
13
|
+
*/
|
|
14
|
+
fun redecode(title: String, charset: String): String {
|
|
15
|
+
var high = false
|
|
16
|
+
for (c in title) {
|
|
17
|
+
if (c.code > 0xFF) return title
|
|
18
|
+
if (c.code >= 0x80) high = true
|
|
19
|
+
}
|
|
20
|
+
if (!high) return title
|
|
21
|
+
val cs = runCatching { Charset.forName(charset) }.getOrNull() ?: return title
|
|
22
|
+
return String(title.toByteArray(Charsets.ISO_8859_1), cs)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -15,11 +15,12 @@ data class PlaybackMetadata(
|
|
|
15
15
|
companion object {
|
|
16
16
|
/**
|
|
17
17
|
* Creates PlaybackMetadata from raw Metadata entries. Handles ICY streams specially, uses
|
|
18
|
-
* Media3's unified approach for everything else.
|
|
18
|
+
* Media3's unified approach for everything else. [icyCharset] is the track's declared charset
|
|
19
|
+
* for ICY titles media3 decoded as Latin-1; it never touches other metadata.
|
|
19
20
|
*/
|
|
20
|
-
fun from(metadata: Metadata): PlaybackMetadata? {
|
|
21
|
+
fun from(metadata: Metadata, icyCharset: String? = null): PlaybackMetadata? {
|
|
21
22
|
// Try ICY first (streaming-specific)
|
|
22
|
-
fromIcy(metadata)?.let {
|
|
23
|
+
fromIcy(metadata, icyCharset)?.let {
|
|
23
24
|
return it
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -32,12 +33,15 @@ data class PlaybackMetadata(
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/** Shoutcast / Icecast metadata (ICY protocol) https://cast.readme.io/docs/icy */
|
|
35
|
-
private fun fromIcy(metadata: Metadata): PlaybackMetadata? {
|
|
36
|
+
private fun fromIcy(metadata: Metadata, icyCharset: String?): PlaybackMetadata? {
|
|
36
37
|
for (i in 0 until metadata.length()) {
|
|
37
38
|
when (val entry = metadata[i]) {
|
|
38
39
|
is IcyInfo -> {
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
val title = entry.title
|
|
41
|
+
if (title != null) {
|
|
42
|
+
return PlaybackMetadata(
|
|
43
|
+
title = icyCharset?.let { IcyTitle.redecode(title, it) } ?: title
|
|
44
|
+
)
|
|
41
45
|
}
|
|
42
46
|
}
|
|
43
47
|
}
|
|
@@ -233,6 +233,7 @@ class PlaybackStateStore(private val player: Player) {
|
|
|
233
233
|
put("disabled", track.disabled)
|
|
234
234
|
put("favorited", track.favorited)
|
|
235
235
|
put("live", track.live)
|
|
236
|
+
put("icyCharset", track.icyCharset)
|
|
236
237
|
}
|
|
237
238
|
.toString()
|
|
238
239
|
|
|
@@ -281,6 +282,7 @@ class PlaybackStateStore(private val player: Player) {
|
|
|
281
282
|
if (obj.has("favorited") && !obj.isNull("favorited")) obj.getBoolean("favorited")
|
|
282
283
|
else null,
|
|
283
284
|
live = if (obj.has("live") && !obj.isNull("live")) obj.getBoolean("live") else null,
|
|
285
|
+
icyCharset = obj.optString("icyCharset").takeIf { it.isNotEmpty() },
|
|
284
286
|
// Not persisted: on resumption the contextual path is re-browsed
|
|
285
287
|
// (expandQueueFromContextualPath), which re-parses each track's request
|
|
286
288
|
// from the API's current response and re-caches it.
|
|
@@ -59,7 +59,7 @@ class PlayerListener(private val player: Player) : MediaPlayer.Listener {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
// Extract and emit timed metadata (ICY, ID3, etc.)
|
|
62
|
-
PlaybackMetadata.from(metadata)?.let {
|
|
62
|
+
PlaybackMetadata.from(metadata, player.currentTrack?.icyCharset)?.let {
|
|
63
63
|
val timed = it.toNitro()
|
|
64
64
|
player.callbacks?.onTimedMetadata(timed)
|
|
65
65
|
player.nowPlaying.onTimedMetadataReceived(timed)
|
|
@@ -55,6 +55,7 @@ object TestFixtures {
|
|
|
55
55
|
disabled = disabled,
|
|
56
56
|
favorited = favorited,
|
|
57
57
|
live = null,
|
|
58
|
+
icyCharset = null,
|
|
58
59
|
)
|
|
59
60
|
|
|
60
61
|
/** A page [Section] — plain list by default. */
|
|
@@ -136,6 +137,7 @@ object TestFixtures {
|
|
|
136
137
|
disabled = null,
|
|
137
138
|
favorited = null,
|
|
138
139
|
live = null,
|
|
140
|
+
icyCharset = null,
|
|
139
141
|
)
|
|
140
142
|
|
|
141
143
|
/** A container [ResolvedTrack] — what a browse route hands back for a path. */
|
|
@@ -168,6 +170,7 @@ object TestFixtures {
|
|
|
168
170
|
disabled = null,
|
|
169
171
|
favorited = null,
|
|
170
172
|
live = null,
|
|
173
|
+
icyCharset = null,
|
|
171
174
|
)
|
|
172
175
|
|
|
173
176
|
/**
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
package com.audiobrowser.model
|
|
2
|
+
|
|
3
|
+
import java.nio.charset.Charset
|
|
4
|
+
import org.junit.Assert.assertEquals
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class IcyTitleTest {
|
|
8
|
+
/** What media3's ISO-8859-1 fallback yields for [text] sent as [charset]. */
|
|
9
|
+
private fun latin1Shaped(text: String, charset: String) =
|
|
10
|
+
String(text.toByteArray(Charset.forName(charset)), Charsets.ISO_8859_1)
|
|
11
|
+
|
|
12
|
+
@Test
|
|
13
|
+
fun `re-decodes a legacy code page title`() {
|
|
14
|
+
val title = latin1Shaped("Артист - Песня", "windows-1251")
|
|
15
|
+
assertEquals("Артист - Песня", IcyTitle.redecode(title, "windows-1251"))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Test
|
|
19
|
+
fun `re-decodes thai`() {
|
|
20
|
+
val title = latin1Shaped("ศิลปิน - เพลง", "windows-874")
|
|
21
|
+
assertEquals("ศิลปิน - เพลง", IcyTitle.redecode(title, "windows-874"))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Test
|
|
25
|
+
fun `re-decodes a title the station utf-8 encoded from legacy bytes`() {
|
|
26
|
+
// media3 decodes the valid UTF-8 to the same Latin-1-shaped string the fallback would give.
|
|
27
|
+
val doubleEncoded = latin1Shaped("Artist - เพลง", "windows-874").toByteArray(Charsets.UTF_8)
|
|
28
|
+
assertEquals(
|
|
29
|
+
"Artist - เพลง",
|
|
30
|
+
IcyTitle.redecode(String(doubleEncoded, Charsets.UTF_8), "windows-874"),
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Test
|
|
35
|
+
fun `leaves a title already in another script`() {
|
|
36
|
+
assertEquals("Песня", IcyTitle.redecode("Песня", "windows-1251"))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Test
|
|
40
|
+
fun `leaves ascii`() {
|
|
41
|
+
assertEquals("Plain Title", IcyTitle.redecode("Plain Title", "windows-1251"))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test
|
|
45
|
+
fun `ignores a charset the runtime cannot decode with`() {
|
|
46
|
+
val title = latin1Shaped("Песня", "windows-1251")
|
|
47
|
+
assertEquals(title, IcyTitle.redecode(title, "no-such-charset"))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -284,6 +284,7 @@ struct JsonResolvedTrack: Codable {
|
|
|
284
284
|
let style: JsonStyle?
|
|
285
285
|
let disabled: Bool?
|
|
286
286
|
let live: Bool?
|
|
287
|
+
let icyCharset: String?
|
|
287
288
|
let carPlaySiriListButton: String?
|
|
288
289
|
|
|
289
290
|
init(
|
|
@@ -304,6 +305,7 @@ struct JsonResolvedTrack: Codable {
|
|
|
304
305
|
style: JsonStyle? = nil,
|
|
305
306
|
disabled: Bool? = nil,
|
|
306
307
|
live: Bool? = nil,
|
|
308
|
+
icyCharset: String? = nil,
|
|
307
309
|
carPlaySiriListButton: String? = nil,
|
|
308
310
|
) {
|
|
309
311
|
self.path = path
|
|
@@ -322,6 +324,7 @@ struct JsonResolvedTrack: Codable {
|
|
|
322
324
|
self.style = style
|
|
323
325
|
self.disabled = disabled
|
|
324
326
|
self.live = live
|
|
327
|
+
self.icyCharset = icyCharset
|
|
325
328
|
self.carPlaySiriListButton = carPlaySiriListButton
|
|
326
329
|
self.id = id
|
|
327
330
|
}
|
|
@@ -345,6 +348,7 @@ struct JsonTrack: Codable {
|
|
|
345
348
|
let style: JsonStyle?
|
|
346
349
|
let disabled: Bool?
|
|
347
350
|
let live: Bool?
|
|
351
|
+
let icyCharset: String?
|
|
348
352
|
|
|
349
353
|
init(
|
|
350
354
|
id: String? = nil,
|
|
@@ -363,6 +367,7 @@ struct JsonTrack: Codable {
|
|
|
363
367
|
style: JsonStyle? = nil,
|
|
364
368
|
disabled: Bool? = nil,
|
|
365
369
|
live: Bool? = nil,
|
|
370
|
+
icyCharset: String? = nil,
|
|
366
371
|
) {
|
|
367
372
|
self.path = path
|
|
368
373
|
self.title = title
|
|
@@ -379,6 +384,7 @@ struct JsonTrack: Codable {
|
|
|
379
384
|
self.style = style
|
|
380
385
|
self.disabled = disabled
|
|
381
386
|
self.live = live
|
|
387
|
+
self.icyCharset = icyCharset
|
|
382
388
|
self.id = id
|
|
383
389
|
}
|
|
384
390
|
}
|
|
@@ -417,6 +423,7 @@ struct JsonTrack: Codable {
|
|
|
417
423
|
style: JsonStyle(track.style),
|
|
418
424
|
disabled: track.disabled,
|
|
419
425
|
live: track.live,
|
|
426
|
+
icyCharset: track.icyCharset,
|
|
420
427
|
)
|
|
421
428
|
}
|
|
422
429
|
}
|
|
@@ -447,6 +454,7 @@ struct JsonTrack: Codable {
|
|
|
447
454
|
style: JsonStyle(track.style),
|
|
448
455
|
disabled: track.disabled,
|
|
449
456
|
live: track.live,
|
|
457
|
+
icyCharset: track.icyCharset,
|
|
450
458
|
)
|
|
451
459
|
}
|
|
452
460
|
}
|
|
@@ -491,6 +499,7 @@ struct JsonTrack: Codable {
|
|
|
491
499
|
disabled: disabled,
|
|
492
500
|
favorited: nil,
|
|
493
501
|
live: live,
|
|
502
|
+
icyCharset: icyCharset,
|
|
494
503
|
)
|
|
495
504
|
}
|
|
496
505
|
}
|
|
@@ -516,6 +525,7 @@ struct JsonTrack: Codable {
|
|
|
516
525
|
disabled: disabled,
|
|
517
526
|
favorited: nil,
|
|
518
527
|
live: live,
|
|
528
|
+
icyCharset: icyCharset,
|
|
519
529
|
)
|
|
520
530
|
}
|
|
521
531
|
}
|
|
@@ -538,6 +548,7 @@ struct JsonTrack: Codable {
|
|
|
538
548
|
style: style?.toTrackStyle(),
|
|
539
549
|
disabled: disabled,
|
|
540
550
|
live: live,
|
|
551
|
+
icyCharset: icyCharset,
|
|
541
552
|
)
|
|
542
553
|
}
|
|
543
554
|
}
|
|
@@ -72,8 +72,10 @@ final class CarPlayImageLoader {
|
|
|
72
72
|
return
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
//
|
|
76
|
-
|
|
75
|
+
// The annotation goes on the closure's return type, not the binding:
|
|
76
|
+
// @Sendable isn't inferred through `map`, and omitting it fails with
|
|
77
|
+
// "type of expression is ambiguous" — which doesn't mention @Sendable.
|
|
78
|
+
let urlResolver = browserManager.map { bm -> @Sendable (Double, Double) async -> ArtworkResolvedImage? in
|
|
77
79
|
{ [weak bm] pixelWidth, pixelHeight in
|
|
78
80
|
guard let bm else { return nil }
|
|
79
81
|
let imageContext = ImageContext(width: pixelWidth, height: pixelHeight)
|
|
@@ -259,7 +259,7 @@ final class CarPlayListItemFactory {
|
|
|
259
259
|
// API (the elements are captured per availability branch — their type
|
|
260
260
|
// doesn't exist before iOS 26). nil = the legacy gridImages path.
|
|
261
261
|
// @MainActor makes the closure Sendable for the artwork completion hop.
|
|
262
|
-
var applyImage: (@MainActor (Int, UIImage) -> Void)?
|
|
262
|
+
var applyImage: (@MainActor @Sendable (Int, UIImage) -> Void)?
|
|
263
263
|
// Resolved once per track, shared by element construction and the
|
|
264
264
|
// artwork loads below.
|
|
265
265
|
let resolvedStyles = tracks.map { StyleResolver.trackStyle(track: $0.style, section: style) }
|
|
@@ -280,7 +280,7 @@ final class CarPlayListItemFactory {
|
|
|
280
280
|
}
|
|
281
281
|
// The image setter every element family shares (`image` is on the base
|
|
282
282
|
// class); bounds-checked against the elements the row was built with.
|
|
283
|
-
func imageApplier(_ elements: [some CPListImageRowItemElement]) -> @MainActor (Int, UIImage) -> Void {
|
|
283
|
+
func imageApplier(_ elements: [some CPListImageRowItemElement]) -> @MainActor @Sendable (Int, UIImage) -> Void {
|
|
284
284
|
{ index, image in
|
|
285
285
|
guard index < elements.count else { return }
|
|
286
286
|
elements[index].image = image
|
|
@@ -386,7 +386,7 @@ final class CarPlayListItemFactory {
|
|
|
386
386
|
for: track,
|
|
387
387
|
style: resolved,
|
|
388
388
|
size: Self.tileImageSize(gridTile: style.gridTile, cardImage: resolved.cardImage),
|
|
389
|
-
) { [weak item] image in
|
|
389
|
+
) { [weak item, applyImage] image in
|
|
390
390
|
Task { @MainActor in
|
|
391
391
|
guard let item, let image else { return }
|
|
392
392
|
if let applyImage {
|
|
@@ -417,7 +417,7 @@ final class CarPlayListItemFactory {
|
|
|
417
417
|
id: nil, path: path, src: nil, artwork: nil, artworkSource: nil,
|
|
418
418
|
request: nil, title: title ?? "", subtitle: nil,
|
|
419
419
|
artist: nil, albumPath: nil, album: nil, description: nil, genre: nil,
|
|
420
|
-
duration: nil, style: nil, disabled: nil, favorited: nil, live: nil,
|
|
420
|
+
duration: nil, style: nil, disabled: nil, favorited: nil, live: nil, icyCharset: nil,
|
|
421
421
|
)
|
|
422
422
|
}
|
|
423
423
|
}
|
|
@@ -33,6 +33,7 @@ extension ResolvedTrack {
|
|
|
33
33
|
disabled: Bool?? = nil,
|
|
34
34
|
favorited: Bool?? = nil,
|
|
35
35
|
live: Bool?? = nil,
|
|
36
|
+
icyCharset: String?? = nil,
|
|
36
37
|
) -> ResolvedTrack {
|
|
37
38
|
ResolvedTrack(
|
|
38
39
|
path: path ?? self.path,
|
|
@@ -56,6 +57,7 @@ extension ResolvedTrack {
|
|
|
56
57
|
disabled: disabled ?? self.disabled,
|
|
57
58
|
favorited: favorited ?? self.favorited,
|
|
58
59
|
live: live ?? self.live,
|
|
60
|
+
icyCharset: icyCharset ?? self.icyCharset,
|
|
59
61
|
)
|
|
60
62
|
}
|
|
61
63
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
extension String {
|
|
4
|
+
/// Re-decodes an ICY `StreamTitle` that AVFoundation decoded as Latin-1 with the
|
|
5
|
+
/// charset the track declares. AVFoundation keeps no raw bytes, so the tell is the
|
|
6
|
+
/// shape: every scalar at or below U+00FF with at least one at or above U+0080. A
|
|
7
|
+
/// title already decoded to another script contains higher scalars and is returned
|
|
8
|
+
/// as is, as is pure ASCII, an unknown charset, or bytes the charset cannot decode.
|
|
9
|
+
func redecodingIcyTitle(charset: String) -> String {
|
|
10
|
+
var high = false
|
|
11
|
+
for scalar in unicodeScalars {
|
|
12
|
+
if scalar.value > 0xFF { return self }
|
|
13
|
+
if scalar.value >= 0x80 { high = true }
|
|
14
|
+
}
|
|
15
|
+
guard high, let data = data(using: .isoLatin1) else { return self }
|
|
16
|
+
let cfEncoding = CFStringConvertIANACharSetNameToEncoding(charset as CFString)
|
|
17
|
+
guard cfEncoding != kCFStringEncodingInvalidId else { return self }
|
|
18
|
+
let encoding = String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(cfEncoding))
|
|
19
|
+
return String(data: data, encoding: encoding) ?? self
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -6,11 +6,13 @@ import AVFoundation
|
|
|
6
6
|
|
|
7
7
|
extension TimedMetadata {
|
|
8
8
|
/// Creates TimedMetadata from AVMetadataItem array, detecting the metadata format.
|
|
9
|
-
|
|
9
|
+
/// `icyCharset` is the track's declared charset for ICY titles AVFoundation
|
|
10
|
+
/// decoded as Latin-1; it never touches ID3 or common metadata.
|
|
11
|
+
static func from(items: [AVMetadataItem], icyCharset: String? = nil) -> TimedMetadata? {
|
|
10
12
|
// Check for ICY metadata (Shoutcast/Icecast streams)
|
|
11
13
|
let icyItems = items.filter { $0.keySpace == .icy }
|
|
12
14
|
if !icyItems.isEmpty {
|
|
13
|
-
return fromIcy(items: icyItems)
|
|
15
|
+
return fromIcy(items: icyItems, icyCharset: icyCharset)
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
// Check for ID3 metadata (MP3)
|
|
@@ -24,7 +26,7 @@ extension TimedMetadata {
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
/// Creates TimedMetadata from ICY (Shoutcast/Icecast) metadata.
|
|
27
|
-
static func fromIcy(items: [AVMetadataItem]) -> TimedMetadata? {
|
|
29
|
+
static func fromIcy(items: [AVMetadataItem], icyCharset: String? = nil) -> TimedMetadata? {
|
|
28
30
|
var title: String?
|
|
29
31
|
|
|
30
32
|
for item in items {
|
|
@@ -35,7 +37,7 @@ extension TimedMetadata {
|
|
|
35
37
|
|
|
36
38
|
guard let title else { return nil }
|
|
37
39
|
return TimedMetadata(
|
|
38
|
-
title: title,
|
|
40
|
+
title: icyCharset.map { title.redecodingIcyTitle(charset: $0) } ?? title,
|
|
39
41
|
artist: nil,
|
|
40
42
|
album: nil,
|
|
41
43
|
date: nil,
|
|
@@ -28,6 +28,7 @@ extension Track {
|
|
|
28
28
|
disabled: Bool?? = nil,
|
|
29
29
|
favorited: Bool?? = nil,
|
|
30
30
|
live: Bool?? = nil,
|
|
31
|
+
icyCharset: String?? = nil,
|
|
31
32
|
) -> Track {
|
|
32
33
|
Track(
|
|
33
34
|
id: id ?? self.id,
|
|
@@ -48,6 +49,7 @@ extension Track {
|
|
|
48
49
|
disabled: disabled ?? self.disabled,
|
|
49
50
|
favorited: favorited ?? self.favorited,
|
|
50
51
|
live: live ?? self.live,
|
|
52
|
+
icyCharset: icyCharset ?? self.icyCharset,
|
|
51
53
|
)
|
|
52
54
|
}
|
|
53
55
|
}
|
|
@@ -1760,7 +1760,7 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1760
1760
|
|
|
1761
1761
|
public func playerDidReceiveTimedMetadata(_ groups: [AVTimedMetadataGroup]) {
|
|
1762
1762
|
for group in groups {
|
|
1763
|
-
if let metadata = TimedMetadata.from(items: group.items) {
|
|
1763
|
+
if let metadata = TimedMetadata.from(items: group.items, icyCharset: player?.currentTrack?.icyCharset) {
|
|
1764
1764
|
latestTimedMetadata = metadata
|
|
1765
1765
|
onTimedMetadata(metadata)
|
|
1766
1766
|
// Re-render the now-playing so the formatter can reflect the live song.
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
var style: TrackStyle?
|
|
43
43
|
var disabled: Bool?
|
|
44
44
|
var live: Bool?
|
|
45
|
+
var icyCharset: String?
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
struct TrackLoadEvent {
|
|
@@ -216,6 +217,7 @@
|
|
|
216
217
|
var disabled: Bool?
|
|
217
218
|
var favorited: Bool?
|
|
218
219
|
var live: Bool?
|
|
220
|
+
var icyCharset: String?
|
|
219
221
|
}
|
|
220
222
|
|
|
221
223
|
enum PlaybackState: Equatable {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// Sendable so LoadSeekCoordinator can carry a delegate into AVPlayer's
|
|
2
|
+
// @Sendable seek completion closure; @MainActor is what keeps that sound.
|
|
3
|
+
@MainActor protocol SeekCompletionHandler: AnyObject, Sendable {
|
|
2
4
|
func handleSeekCompleted(to seconds: Double, didFinish: Bool)
|
|
3
5
|
}
|
|
@@ -18,7 +18,7 @@ struct PlaybackStateStoreTests {
|
|
|
18
18
|
artist: "City, Country", albumPath: nil, album: nil, description: nil,
|
|
19
19
|
genre: nil, duration: nil, src: "/listen/abc/channel.mp3", request: nil,
|
|
20
20
|
style: JsonStyle(display: "grid", artworkRendering: "stencil"),
|
|
21
|
-
disabled: false, live: true,
|
|
21
|
+
disabled: false, live: true, icyCharset: "windows-1251",
|
|
22
22
|
)
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -25,6 +25,7 @@ private func makeResolvedTrack(
|
|
|
25
25
|
disabled: nil,
|
|
26
26
|
favorited: nil,
|
|
27
27
|
live: nil,
|
|
28
|
+
icyCharset: nil,
|
|
28
29
|
)
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -49,6 +50,7 @@ private func makeFullResolvedTrack() -> ResolvedTrack {
|
|
|
49
50
|
disabled: false,
|
|
50
51
|
favorited: true,
|
|
51
52
|
live: false,
|
|
53
|
+
icyCharset: nil,
|
|
52
54
|
)
|
|
53
55
|
}
|
|
54
56
|
|
|
@@ -20,6 +20,48 @@ struct TimedMetadataICYTests {
|
|
|
20
20
|
#expect(result?.artist == nil)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
private func icyTitle(_ value: String) -> AVMetadataItem {
|
|
24
|
+
let item = AVMutableMetadataItem()
|
|
25
|
+
item.key = "StreamTitle" as NSString
|
|
26
|
+
item.keySpace = .icy
|
|
27
|
+
item.value = value as NSString
|
|
28
|
+
return item
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// "Песня" as windows-1251 bytes, decoded as Latin-1 the way AVFoundation does when it guesses wrong.
|
|
32
|
+
private let latin1Shaped = "\u{CF}\u{E5}\u{F1}\u{ED}\u{FF}"
|
|
33
|
+
|
|
34
|
+
@Test func icyCharset_redecodesLatin1ShapedTitle() {
|
|
35
|
+
let result = TimedMetadata.from(items: [icyTitle(latin1Shaped)], icyCharset: "windows-1251")
|
|
36
|
+
#expect(result?.title == "Песня")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Test func icyCharset_leavesAlreadyDecodedTitle() {
|
|
40
|
+
let result = TimedMetadata.from(items: [icyTitle("Песня")], icyCharset: "windows-1251")
|
|
41
|
+
#expect(result?.title == "Песня")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test func icyCharset_leavesAscii() {
|
|
45
|
+
let result = TimedMetadata.from(items: [icyTitle("Plain Title")], icyCharset: "windows-1251")
|
|
46
|
+
#expect(result?.title == "Plain Title")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Test func icyCharset_ignoresUnknownCharset() {
|
|
50
|
+
let result = TimedMetadata.from(items: [icyTitle(latin1Shaped)], icyCharset: "no-such-charset")
|
|
51
|
+
#expect(result?.title == latin1Shaped)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Test func icyCharset_absent_keepsTitle() {
|
|
55
|
+
let result = TimedMetadata.from(items: [icyTitle(latin1Shaped)])
|
|
56
|
+
#expect(result?.title == latin1Shaped)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Test func icyCharset_neverTouchesId3() {
|
|
60
|
+
let items = [makeMetadataItem(identifier: .id3MetadataTitleDescription, value: latin1Shaped)]
|
|
61
|
+
let result = TimedMetadata.from(items: items, icyCharset: "windows-1251")
|
|
62
|
+
#expect(result?.title == latin1Shaped)
|
|
63
|
+
}
|
|
64
|
+
|
|
23
65
|
@Test func icyWithoutStreamTitle_returnsNil() {
|
|
24
66
|
let item = AVMutableMetadataItem()
|
|
25
67
|
item.key = "StreamUrl" as NSString
|
|
@@ -505,6 +505,20 @@ export interface Track {
|
|
|
505
505
|
* calls. Declare it on every live track.
|
|
506
506
|
*/
|
|
507
507
|
live?: boolean;
|
|
508
|
+
/**
|
|
509
|
+
* Byte encoding of this track's ICY `StreamTitle` when it is not UTF-8, as
|
|
510
|
+
* an IANA label (`'windows-1251'`, `'windows-874'`, …). Some Shoutcast /
|
|
511
|
+
* Icecast stations send titles in a legacy code page; ICY declares no
|
|
512
|
+
* encoding, so the player's UTF-8 decode fails and falls back to Latin-1,
|
|
513
|
+
* showing mojibake. With this set, a title that came out Latin-1 shaped
|
|
514
|
+
* (every char at or below U+00FF, some at or above U+0080) is re-decoded
|
|
515
|
+
* through ISO-8859-1 with the given charset; this also repairs stations
|
|
516
|
+
* that UTF-8 encoded their legacy bytes. A title already in another script
|
|
517
|
+
* or pure ASCII is left alone. Applies to ICY only: ID3 and HLS timed
|
|
518
|
+
* metadata declare their own encoding and are never touched. A label the
|
|
519
|
+
* platform cannot decode with is ignored.
|
|
520
|
+
*/
|
|
521
|
+
icyCharset?: string;
|
|
508
522
|
}
|
|
509
523
|
/**
|
|
510
524
|
* A Track that has been resolved with its children through browsing.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-nodes.d.ts","sourceRoot":"","sources":["../../../../src/types/browser-nodes.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"browser-nodes.d.ts","sourceRoot":"","sources":["../../../../src/types/browser-nodes.ts"],"names":[],"mappings":"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"}
|
|
@@ -106,6 +106,8 @@ namespace margelo::nitro::audiobrowser {
|
|
|
106
106
|
jni::local_ref<jni::JBoolean> favorited = this->getFieldValue(fieldFavorited);
|
|
107
107
|
static const auto fieldLive = clazz->getField<jni::JBoolean>("live");
|
|
108
108
|
jni::local_ref<jni::JBoolean> live = this->getFieldValue(fieldLive);
|
|
109
|
+
static const auto fieldIcyCharset = clazz->getField<jni::JString>("icyCharset");
|
|
110
|
+
jni::local_ref<jni::JString> icyCharset = this->getFieldValue(fieldIcyCharset);
|
|
109
111
|
return ResolvedTrack(
|
|
110
112
|
path->toStdString(),
|
|
111
113
|
style != nullptr ? std::make_optional(style->toCpp()) : std::nullopt,
|
|
@@ -145,7 +147,8 @@ namespace margelo::nitro::audiobrowser {
|
|
|
145
147
|
duration != nullptr ? std::make_optional(duration->value()) : std::nullopt,
|
|
146
148
|
disabled != nullptr ? std::make_optional(static_cast<bool>(disabled->value())) : std::nullopt,
|
|
147
149
|
favorited != nullptr ? std::make_optional(static_cast<bool>(favorited->value())) : std::nullopt,
|
|
148
|
-
live != nullptr ? std::make_optional(static_cast<bool>(live->value())) : std::nullopt
|
|
150
|
+
live != nullptr ? std::make_optional(static_cast<bool>(live->value())) : std::nullopt,
|
|
151
|
+
icyCharset != nullptr ? std::make_optional(icyCharset->toStdString()) : std::nullopt
|
|
149
152
|
);
|
|
150
153
|
}
|
|
151
154
|
|
|
@@ -155,7 +158,7 @@ namespace margelo::nitro::audiobrowser {
|
|
|
155
158
|
*/
|
|
156
159
|
[[maybe_unused]]
|
|
157
160
|
static jni::local_ref<JResolvedTrack::javaobject> fromCpp(const ResolvedTrack& value) {
|
|
158
|
-
using JSignature = JResolvedTrack(jni::alias_ref<jni::JString>, jni::alias_ref<JSectionStyle>, jni::alias_ref<jni::JArrayClass<JSection>>, jni::alias_ref<jni::JArrayClass<JTrack>>, jni::alias_ref<JCarPlaySiriListButtonPosition>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<JVariant_String_ArtworkVariants>, jni::alias_ref<JImageSource>, jni::alias_ref<JTrackRequest>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JDouble>, jni::alias_ref<jni::JBoolean>, jni::alias_ref<jni::JBoolean>, jni::alias_ref<jni::JBoolean>);
|
|
161
|
+
using JSignature = JResolvedTrack(jni::alias_ref<jni::JString>, jni::alias_ref<JSectionStyle>, jni::alias_ref<jni::JArrayClass<JSection>>, jni::alias_ref<jni::JArrayClass<JTrack>>, jni::alias_ref<JCarPlaySiriListButtonPosition>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<JVariant_String_ArtworkVariants>, jni::alias_ref<JImageSource>, jni::alias_ref<JTrackRequest>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JDouble>, jni::alias_ref<jni::JBoolean>, jni::alias_ref<jni::JBoolean>, jni::alias_ref<jni::JBoolean>, jni::alias_ref<jni::JString>);
|
|
159
162
|
static const auto clazz = javaClassStatic();
|
|
160
163
|
static const auto create = clazz->getStaticMethod<JSignature>("fromCpp");
|
|
161
164
|
return create(
|
|
@@ -198,7 +201,8 @@ namespace margelo::nitro::audiobrowser {
|
|
|
198
201
|
value.duration.has_value() ? jni::JDouble::valueOf(value.duration.value()) : nullptr,
|
|
199
202
|
value.disabled.has_value() ? jni::JBoolean::valueOf(value.disabled.value()) : nullptr,
|
|
200
203
|
value.favorited.has_value() ? jni::JBoolean::valueOf(value.favorited.value()) : nullptr,
|
|
201
|
-
value.live.has_value() ? jni::JBoolean::valueOf(value.live.value()) : nullptr
|
|
204
|
+
value.live.has_value() ? jni::JBoolean::valueOf(value.live.value()) : nullptr,
|
|
205
|
+
value.icyCharset.has_value() ? jni::make_jstring(value.icyCharset.value()) : nullptr
|
|
202
206
|
);
|
|
203
207
|
}
|
|
204
208
|
};
|
|
@@ -89,6 +89,8 @@ namespace margelo::nitro::audiobrowser {
|
|
|
89
89
|
jni::local_ref<jni::JBoolean> favorited = this->getFieldValue(fieldFavorited);
|
|
90
90
|
static const auto fieldLive = clazz->getField<jni::JBoolean>("live");
|
|
91
91
|
jni::local_ref<jni::JBoolean> live = this->getFieldValue(fieldLive);
|
|
92
|
+
static const auto fieldIcyCharset = clazz->getField<jni::JString>("icyCharset");
|
|
93
|
+
jni::local_ref<jni::JString> icyCharset = this->getFieldValue(fieldIcyCharset);
|
|
92
94
|
return Track(
|
|
93
95
|
id != nullptr ? std::make_optional(id->toStdString()) : std::nullopt,
|
|
94
96
|
path != nullptr ? std::make_optional(path->toStdString()) : std::nullopt,
|
|
@@ -107,7 +109,8 @@ namespace margelo::nitro::audiobrowser {
|
|
|
107
109
|
style != nullptr ? std::make_optional(style->toCpp()) : std::nullopt,
|
|
108
110
|
disabled != nullptr ? std::make_optional(static_cast<bool>(disabled->value())) : std::nullopt,
|
|
109
111
|
favorited != nullptr ? std::make_optional(static_cast<bool>(favorited->value())) : std::nullopt,
|
|
110
|
-
live != nullptr ? std::make_optional(static_cast<bool>(live->value())) : std::nullopt
|
|
112
|
+
live != nullptr ? std::make_optional(static_cast<bool>(live->value())) : std::nullopt,
|
|
113
|
+
icyCharset != nullptr ? std::make_optional(icyCharset->toStdString()) : std::nullopt
|
|
111
114
|
);
|
|
112
115
|
}
|
|
113
116
|
|
|
@@ -117,7 +120,7 @@ namespace margelo::nitro::audiobrowser {
|
|
|
117
120
|
*/
|
|
118
121
|
[[maybe_unused]]
|
|
119
122
|
static jni::local_ref<JTrack::javaobject> fromCpp(const Track& value) {
|
|
120
|
-
using JSignature = JTrack(jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<JVariant_String_ArtworkVariants>, jni::alias_ref<JImageSource>, jni::alias_ref<JTrackRequest>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JDouble>, jni::alias_ref<JTrackStyle>, jni::alias_ref<jni::JBoolean>, jni::alias_ref<jni::JBoolean>, jni::alias_ref<jni::JBoolean>);
|
|
123
|
+
using JSignature = JTrack(jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<JVariant_String_ArtworkVariants>, jni::alias_ref<JImageSource>, jni::alias_ref<JTrackRequest>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JDouble>, jni::alias_ref<JTrackStyle>, jni::alias_ref<jni::JBoolean>, jni::alias_ref<jni::JBoolean>, jni::alias_ref<jni::JBoolean>, jni::alias_ref<jni::JString>);
|
|
121
124
|
static const auto clazz = javaClassStatic();
|
|
122
125
|
static const auto create = clazz->getStaticMethod<JSignature>("fromCpp");
|
|
123
126
|
return create(
|
|
@@ -139,7 +142,8 @@ namespace margelo::nitro::audiobrowser {
|
|
|
139
142
|
value.style.has_value() ? JTrackStyle::fromCpp(value.style.value()) : nullptr,
|
|
140
143
|
value.disabled.has_value() ? jni::JBoolean::valueOf(value.disabled.value()) : nullptr,
|
|
141
144
|
value.favorited.has_value() ? jni::JBoolean::valueOf(value.favorited.value()) : nullptr,
|
|
142
|
-
value.live.has_value() ? jni::JBoolean::valueOf(value.live.value()) : nullptr
|
|
145
|
+
value.live.has_value() ? jni::JBoolean::valueOf(value.live.value()) : nullptr,
|
|
146
|
+
value.icyCharset.has_value() ? jni::make_jstring(value.icyCharset.value()) : nullptr
|
|
143
147
|
);
|
|
144
148
|
}
|
|
145
149
|
};
|
|
@@ -79,7 +79,10 @@ data class ResolvedTrack(
|
|
|
79
79
|
val favorited: Boolean?,
|
|
80
80
|
@DoNotStrip
|
|
81
81
|
@Keep
|
|
82
|
-
val live: Boolean
|
|
82
|
+
val live: Boolean?,
|
|
83
|
+
@DoNotStrip
|
|
84
|
+
@Keep
|
|
85
|
+
val icyCharset: String?
|
|
83
86
|
) {
|
|
84
87
|
/* primary constructor */
|
|
85
88
|
|
|
@@ -91,8 +94,8 @@ data class ResolvedTrack(
|
|
|
91
94
|
@Keep
|
|
92
95
|
@Suppress("unused")
|
|
93
96
|
@JvmStatic
|
|
94
|
-
private fun fromCpp(path: String, style: SectionStyle?, sections: Array<Section>?, children: Array<Track>?, carPlaySiriListButton: CarPlaySiriListButtonPosition?, id: String?, src: String?, artwork: Variant_String_ArtworkVariants?, artworkSource: ImageSource?, request: TrackRequest?, title: String, subtitle: String?, artist: String?, albumPath: String?, album: String?, description: String?, genre: String?, duration: Double?, disabled: Boolean?, favorited: Boolean?, live: Boolean?): ResolvedTrack {
|
|
95
|
-
return ResolvedTrack(path, style, sections, children, carPlaySiriListButton, id, src, artwork, artworkSource, request, title, subtitle, artist, albumPath, album, description, genre, duration, disabled, favorited, live)
|
|
97
|
+
private fun fromCpp(path: String, style: SectionStyle?, sections: Array<Section>?, children: Array<Track>?, carPlaySiriListButton: CarPlaySiriListButtonPosition?, id: String?, src: String?, artwork: Variant_String_ArtworkVariants?, artworkSource: ImageSource?, request: TrackRequest?, title: String, subtitle: String?, artist: String?, albumPath: String?, album: String?, description: String?, genre: String?, duration: Double?, disabled: Boolean?, favorited: Boolean?, live: Boolean?, icyCharset: String?): ResolvedTrack {
|
|
98
|
+
return ResolvedTrack(path, style, sections, children, carPlaySiriListButton, id, src, artwork, artworkSource, request, title, subtitle, artist, albumPath, album, description, genre, duration, disabled, favorited, live, icyCharset)
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
101
|
}
|
|
@@ -70,7 +70,10 @@ data class Track(
|
|
|
70
70
|
val favorited: Boolean?,
|
|
71
71
|
@DoNotStrip
|
|
72
72
|
@Keep
|
|
73
|
-
val live: Boolean
|
|
73
|
+
val live: Boolean?,
|
|
74
|
+
@DoNotStrip
|
|
75
|
+
@Keep
|
|
76
|
+
val icyCharset: String?
|
|
74
77
|
) {
|
|
75
78
|
/* primary constructor */
|
|
76
79
|
|
|
@@ -82,8 +85,8 @@ data class Track(
|
|
|
82
85
|
@Keep
|
|
83
86
|
@Suppress("unused")
|
|
84
87
|
@JvmStatic
|
|
85
|
-
private fun fromCpp(id: String?, path: String?, src: String?, artwork: Variant_String_ArtworkVariants?, artworkSource: ImageSource?, request: TrackRequest?, title: String, subtitle: String?, artist: String?, albumPath: String?, album: String?, description: String?, genre: String?, duration: Double?, style: TrackStyle?, disabled: Boolean?, favorited: Boolean?, live: Boolean?): Track {
|
|
86
|
-
return Track(id, path, src, artwork, artworkSource, request, title, subtitle, artist, albumPath, album, description, genre, duration, style, disabled, favorited, live)
|
|
88
|
+
private fun fromCpp(id: String?, path: String?, src: String?, artwork: Variant_String_ArtworkVariants?, artworkSource: ImageSource?, request: TrackRequest?, title: String, subtitle: String?, artist: String?, albumPath: String?, album: String?, description: String?, genre: String?, duration: Double?, style: TrackStyle?, disabled: Boolean?, favorited: Boolean?, live: Boolean?, icyCharset: String?): Track {
|
|
89
|
+
return Track(id, path, src, artwork, artworkSource, request, title, subtitle, artist, albumPath, album, description, genre, duration, style, disabled, favorited, live, icyCharset)
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
92
|
}
|
|
@@ -18,7 +18,7 @@ public extension ResolvedTrack {
|
|
|
18
18
|
/**
|
|
19
19
|
* Create a new instance of `ResolvedTrack`.
|
|
20
20
|
*/
|
|
21
|
-
init(path: String, style: SectionStyle?, sections: [Section]?, children: [Track]?, carPlaySiriListButton: CarPlaySiriListButtonPosition?, id: String?, src: String?, artwork: Variant_String_ArtworkVariants?, artworkSource: ImageSource?, request: TrackRequest?, title: String, subtitle: String?, artist: String?, albumPath: String?, album: String?, description: String?, genre: String?, duration: Double?, disabled: Bool?, favorited: Bool?, live: Bool?) {
|
|
21
|
+
init(path: String, style: SectionStyle?, sections: [Section]?, children: [Track]?, carPlaySiriListButton: CarPlaySiriListButtonPosition?, id: String?, src: String?, artwork: Variant_String_ArtworkVariants?, artworkSource: ImageSource?, request: TrackRequest?, title: String, subtitle: String?, artist: String?, albumPath: String?, album: String?, description: String?, genre: String?, duration: Double?, disabled: Bool?, favorited: Bool?, live: Bool?, icyCharset: String?) {
|
|
22
22
|
self.init(std.string(path), { () -> bridge.std__optional_SectionStyle_ in
|
|
23
23
|
if let __unwrappedValue = style {
|
|
24
24
|
return bridge.create_std__optional_SectionStyle_(__unwrappedValue)
|
|
@@ -152,6 +152,12 @@ public extension ResolvedTrack {
|
|
|
152
152
|
} else {
|
|
153
153
|
return .init()
|
|
154
154
|
}
|
|
155
|
+
}(), { () -> bridge.std__optional_std__string_ in
|
|
156
|
+
if let __unwrappedValue = icyCharset {
|
|
157
|
+
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
|
|
158
|
+
} else {
|
|
159
|
+
return .init()
|
|
160
|
+
}
|
|
155
161
|
}())
|
|
156
162
|
}
|
|
157
163
|
|
|
@@ -376,4 +382,16 @@ public extension ResolvedTrack {
|
|
|
376
382
|
}
|
|
377
383
|
}()
|
|
378
384
|
}
|
|
385
|
+
|
|
386
|
+
@inline(__always)
|
|
387
|
+
var icyCharset: String? {
|
|
388
|
+
return { () -> String? in
|
|
389
|
+
if bridge.has_value_std__optional_std__string_(self.__icyCharset) {
|
|
390
|
+
let __unwrapped = bridge.get_std__optional_std__string_(self.__icyCharset)
|
|
391
|
+
return String(__unwrapped)
|
|
392
|
+
} else {
|
|
393
|
+
return nil
|
|
394
|
+
}
|
|
395
|
+
}()
|
|
396
|
+
}
|
|
379
397
|
}
|
|
@@ -18,7 +18,7 @@ public extension Track {
|
|
|
18
18
|
/**
|
|
19
19
|
* Create a new instance of `Track`.
|
|
20
20
|
*/
|
|
21
|
-
init(id: String?, path: String?, src: String?, artwork: Variant_String_ArtworkVariants?, artworkSource: ImageSource?, request: TrackRequest?, title: String, subtitle: String?, artist: String?, albumPath: String?, album: String?, description: String?, genre: String?, duration: Double?, style: TrackStyle?, disabled: Bool?, favorited: Bool?, live: Bool?) {
|
|
21
|
+
init(id: String?, path: String?, src: String?, artwork: Variant_String_ArtworkVariants?, artworkSource: ImageSource?, request: TrackRequest?, title: String, subtitle: String?, artist: String?, albumPath: String?, album: String?, description: String?, genre: String?, duration: Double?, style: TrackStyle?, disabled: Bool?, favorited: Bool?, live: Bool?, icyCharset: String?) {
|
|
22
22
|
self.init({ () -> bridge.std__optional_std__string_ in
|
|
23
23
|
if let __unwrappedValue = id {
|
|
24
24
|
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
|
|
@@ -128,6 +128,12 @@ public extension Track {
|
|
|
128
128
|
} else {
|
|
129
129
|
return .init()
|
|
130
130
|
}
|
|
131
|
+
}(), { () -> bridge.std__optional_std__string_ in
|
|
132
|
+
if let __unwrappedValue = icyCharset {
|
|
133
|
+
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
|
|
134
|
+
} else {
|
|
135
|
+
return .init()
|
|
136
|
+
}
|
|
131
137
|
}())
|
|
132
138
|
}
|
|
133
139
|
|
|
@@ -330,4 +336,16 @@ public extension Track {
|
|
|
330
336
|
}
|
|
331
337
|
}()
|
|
332
338
|
}
|
|
339
|
+
|
|
340
|
+
@inline(__always)
|
|
341
|
+
var icyCharset: String? {
|
|
342
|
+
return { () -> String? in
|
|
343
|
+
if bridge.has_value_std__optional_std__string_(self.__icyCharset) {
|
|
344
|
+
let __unwrapped = bridge.get_std__optional_std__string_(self.__icyCharset)
|
|
345
|
+
return String(__unwrapped)
|
|
346
|
+
} else {
|
|
347
|
+
return nil
|
|
348
|
+
}
|
|
349
|
+
}()
|
|
350
|
+
}
|
|
333
351
|
}
|
|
@@ -83,10 +83,11 @@ namespace margelo::nitro::audiobrowser {
|
|
|
83
83
|
std::optional<bool> disabled SWIFT_PRIVATE;
|
|
84
84
|
std::optional<bool> favorited SWIFT_PRIVATE;
|
|
85
85
|
std::optional<bool> live SWIFT_PRIVATE;
|
|
86
|
+
std::optional<std::string> icyCharset SWIFT_PRIVATE;
|
|
86
87
|
|
|
87
88
|
public:
|
|
88
89
|
ResolvedTrack() = default;
|
|
89
|
-
explicit ResolvedTrack(std::string path, std::optional<SectionStyle> style, std::optional<std::vector<Section>> sections, std::optional<std::vector<Track>> children, std::optional<CarPlaySiriListButtonPosition> carPlaySiriListButton, std::optional<std::string> id, std::optional<std::string> src, std::optional<std::variant<std::string, ArtworkVariants>> artwork, std::optional<ImageSource> artworkSource, std::optional<TrackRequest> request, std::string title, std::optional<std::string> subtitle, std::optional<std::string> artist, std::optional<std::string> albumPath, std::optional<std::string> album, std::optional<std::string> description, std::optional<std::string> genre, std::optional<double> duration, std::optional<bool> disabled, std::optional<bool> favorited, std::optional<bool> live): path(path), style(style), sections(sections), children(children), carPlaySiriListButton(carPlaySiriListButton), id(id), src(src), artwork(artwork), artworkSource(artworkSource), request(request), title(title), subtitle(subtitle), artist(artist), albumPath(albumPath), album(album), description(description), genre(genre), duration(duration), disabled(disabled), favorited(favorited), live(live) {}
|
|
90
|
+
explicit ResolvedTrack(std::string path, std::optional<SectionStyle> style, std::optional<std::vector<Section>> sections, std::optional<std::vector<Track>> children, std::optional<CarPlaySiriListButtonPosition> carPlaySiriListButton, std::optional<std::string> id, std::optional<std::string> src, std::optional<std::variant<std::string, ArtworkVariants>> artwork, std::optional<ImageSource> artworkSource, std::optional<TrackRequest> request, std::string title, std::optional<std::string> subtitle, std::optional<std::string> artist, std::optional<std::string> albumPath, std::optional<std::string> album, std::optional<std::string> description, std::optional<std::string> genre, std::optional<double> duration, std::optional<bool> disabled, std::optional<bool> favorited, std::optional<bool> live, std::optional<std::string> icyCharset): path(path), style(style), sections(sections), children(children), carPlaySiriListButton(carPlaySiriListButton), id(id), src(src), artwork(artwork), artworkSource(artworkSource), request(request), title(title), subtitle(subtitle), artist(artist), albumPath(albumPath), album(album), description(description), genre(genre), duration(duration), disabled(disabled), favorited(favorited), live(live), icyCharset(icyCharset) {}
|
|
90
91
|
|
|
91
92
|
public:
|
|
92
93
|
friend bool operator==(const ResolvedTrack& lhs, const ResolvedTrack& rhs) = default;
|
|
@@ -122,7 +123,8 @@ namespace margelo::nitro {
|
|
|
122
123
|
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "duration"))),
|
|
123
124
|
JSIConverter<std::optional<bool>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "disabled"))),
|
|
124
125
|
JSIConverter<std::optional<bool>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "favorited"))),
|
|
125
|
-
JSIConverter<std::optional<bool>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "live")))
|
|
126
|
+
JSIConverter<std::optional<bool>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "live"))),
|
|
127
|
+
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "icyCharset")))
|
|
126
128
|
);
|
|
127
129
|
}
|
|
128
130
|
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::audiobrowser::ResolvedTrack& arg) {
|
|
@@ -148,6 +150,7 @@ namespace margelo::nitro {
|
|
|
148
150
|
obj.setProperty(runtime, PropNameIDCache::get(runtime, "disabled"), JSIConverter<std::optional<bool>>::toJSI(runtime, arg.disabled));
|
|
149
151
|
obj.setProperty(runtime, PropNameIDCache::get(runtime, "favorited"), JSIConverter<std::optional<bool>>::toJSI(runtime, arg.favorited));
|
|
150
152
|
obj.setProperty(runtime, PropNameIDCache::get(runtime, "live"), JSIConverter<std::optional<bool>>::toJSI(runtime, arg.live));
|
|
153
|
+
obj.setProperty(runtime, PropNameIDCache::get(runtime, "icyCharset"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.icyCharset));
|
|
151
154
|
return obj;
|
|
152
155
|
}
|
|
153
156
|
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
|
|
@@ -179,6 +182,7 @@ namespace margelo::nitro {
|
|
|
179
182
|
if (!JSIConverter<std::optional<bool>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "disabled")))) return false;
|
|
180
183
|
if (!JSIConverter<std::optional<bool>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "favorited")))) return false;
|
|
181
184
|
if (!JSIConverter<std::optional<bool>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "live")))) return false;
|
|
185
|
+
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "icyCharset")))) return false;
|
|
182
186
|
return true;
|
|
183
187
|
}
|
|
184
188
|
};
|
|
@@ -70,10 +70,11 @@ namespace margelo::nitro::audiobrowser {
|
|
|
70
70
|
std::optional<bool> disabled SWIFT_PRIVATE;
|
|
71
71
|
std::optional<bool> favorited SWIFT_PRIVATE;
|
|
72
72
|
std::optional<bool> live SWIFT_PRIVATE;
|
|
73
|
+
std::optional<std::string> icyCharset SWIFT_PRIVATE;
|
|
73
74
|
|
|
74
75
|
public:
|
|
75
76
|
Track() = default;
|
|
76
|
-
explicit Track(std::optional<std::string> id, std::optional<std::string> path, std::optional<std::string> src, std::optional<std::variant<std::string, ArtworkVariants>> artwork, std::optional<ImageSource> artworkSource, std::optional<TrackRequest> request, std::string title, std::optional<std::string> subtitle, std::optional<std::string> artist, std::optional<std::string> albumPath, std::optional<std::string> album, std::optional<std::string> description, std::optional<std::string> genre, std::optional<double> duration, std::optional<TrackStyle> style, std::optional<bool> disabled, std::optional<bool> favorited, std::optional<bool> live): id(id), path(path), src(src), artwork(artwork), artworkSource(artworkSource), request(request), title(title), subtitle(subtitle), artist(artist), albumPath(albumPath), album(album), description(description), genre(genre), duration(duration), style(style), disabled(disabled), favorited(favorited), live(live) {}
|
|
77
|
+
explicit Track(std::optional<std::string> id, std::optional<std::string> path, std::optional<std::string> src, std::optional<std::variant<std::string, ArtworkVariants>> artwork, std::optional<ImageSource> artworkSource, std::optional<TrackRequest> request, std::string title, std::optional<std::string> subtitle, std::optional<std::string> artist, std::optional<std::string> albumPath, std::optional<std::string> album, std::optional<std::string> description, std::optional<std::string> genre, std::optional<double> duration, std::optional<TrackStyle> style, std::optional<bool> disabled, std::optional<bool> favorited, std::optional<bool> live, std::optional<std::string> icyCharset): id(id), path(path), src(src), artwork(artwork), artworkSource(artworkSource), request(request), title(title), subtitle(subtitle), artist(artist), albumPath(albumPath), album(album), description(description), genre(genre), duration(duration), style(style), disabled(disabled), favorited(favorited), live(live), icyCharset(icyCharset) {}
|
|
77
78
|
|
|
78
79
|
public:
|
|
79
80
|
friend bool operator==(const Track& lhs, const Track& rhs) = default;
|
|
@@ -106,7 +107,8 @@ namespace margelo::nitro {
|
|
|
106
107
|
JSIConverter<std::optional<margelo::nitro::audiobrowser::TrackStyle>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "style"))),
|
|
107
108
|
JSIConverter<std::optional<bool>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "disabled"))),
|
|
108
109
|
JSIConverter<std::optional<bool>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "favorited"))),
|
|
109
|
-
JSIConverter<std::optional<bool>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "live")))
|
|
110
|
+
JSIConverter<std::optional<bool>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "live"))),
|
|
111
|
+
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "icyCharset")))
|
|
110
112
|
);
|
|
111
113
|
}
|
|
112
114
|
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::audiobrowser::Track& arg) {
|
|
@@ -129,6 +131,7 @@ namespace margelo::nitro {
|
|
|
129
131
|
obj.setProperty(runtime, PropNameIDCache::get(runtime, "disabled"), JSIConverter<std::optional<bool>>::toJSI(runtime, arg.disabled));
|
|
130
132
|
obj.setProperty(runtime, PropNameIDCache::get(runtime, "favorited"), JSIConverter<std::optional<bool>>::toJSI(runtime, arg.favorited));
|
|
131
133
|
obj.setProperty(runtime, PropNameIDCache::get(runtime, "live"), JSIConverter<std::optional<bool>>::toJSI(runtime, arg.live));
|
|
134
|
+
obj.setProperty(runtime, PropNameIDCache::get(runtime, "icyCharset"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.icyCharset));
|
|
132
135
|
return obj;
|
|
133
136
|
}
|
|
134
137
|
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
|
|
@@ -157,6 +160,7 @@ namespace margelo::nitro {
|
|
|
157
160
|
if (!JSIConverter<std::optional<bool>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "disabled")))) return false;
|
|
158
161
|
if (!JSIConverter<std::optional<bool>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "favorited")))) return false;
|
|
159
162
|
if (!JSIConverter<std::optional<bool>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "live")))) return false;
|
|
163
|
+
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "icyCharset")))) return false;
|
|
160
164
|
return true;
|
|
161
165
|
}
|
|
162
166
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-audio-browser",
|
|
3
|
-
"version": "0.2.0-next.
|
|
3
|
+
"version": "0.2.0-next.16",
|
|
4
4
|
"description": "react-native-audio-browser is a react native package built with Nitro",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -60,11 +60,11 @@
|
|
|
60
60
|
"example": "yarn workspace example-native",
|
|
61
61
|
"ci:android": "yarn android:format:check && yarn android:types && yarn android:test",
|
|
62
62
|
"ci:format": "oxfmt --check --ignore-path .gitignore --ignore-path .oxfmtignore .",
|
|
63
|
-
"ci:format:ios": "swiftformat --lint ios",
|
|
63
|
+
"ci:format:ios": "scripts/swiftformat.sh --lint ios",
|
|
64
64
|
"ci:ios": "yarn ci:format:ios && yarn ios:test",
|
|
65
65
|
"ci:lint": "oxlint --type-aware src",
|
|
66
66
|
"format": "oxfmt --write --ignore-path .gitignore --ignore-path .oxfmtignore .",
|
|
67
|
-
"ios:format": "swiftformat ios",
|
|
67
|
+
"ios:format": "scripts/swiftformat.sh ios",
|
|
68
68
|
"ios:ide": "yarn workspace example-native ios:ide",
|
|
69
69
|
"ios:rebuild": "yarn workspace example-native ios:rebuild",
|
|
70
70
|
"ios:test": "swift test --disable-sandbox",
|
|
@@ -80,21 +80,21 @@
|
|
|
80
80
|
"@testing-library/dom": "^10.4.1",
|
|
81
81
|
"@testing-library/react": "^16.3.2",
|
|
82
82
|
"@types/node": "^22",
|
|
83
|
-
"@types/react": "19.
|
|
84
|
-
"@types/react-dom": "19.
|
|
83
|
+
"@types/react": "19.2.3",
|
|
84
|
+
"@types/react-dom": "^19.2.0",
|
|
85
85
|
"conventional-changelog-conventionalcommits": "^9.3.0",
|
|
86
86
|
"del-cli": "^5.1.0",
|
|
87
87
|
"happy-dom": "^20.11.2",
|
|
88
|
-
"nitrogen": "0.35.
|
|
88
|
+
"nitrogen": "0.35.10",
|
|
89
89
|
"oxfmt": "^0.27.0",
|
|
90
90
|
"oxlint": "^1.42.0",
|
|
91
91
|
"oxlint-tsgolint": "^0.11.2",
|
|
92
|
-
"react": "19.
|
|
93
|
-
"react-dom": "19.
|
|
94
|
-
"react-native": "0.
|
|
92
|
+
"react": "19.2.3",
|
|
93
|
+
"react-dom": "19.2.3",
|
|
94
|
+
"react-native": "0.86.2",
|
|
95
95
|
"react-native-builder-bob": "^0.37.0",
|
|
96
96
|
"react-native-monorepo-config": "^0.3.1",
|
|
97
|
-
"react-native-nitro-modules": "0.35.
|
|
97
|
+
"react-native-nitro-modules": "0.35.10",
|
|
98
98
|
"semantic-release": "^25.0.3",
|
|
99
99
|
"shaka-player": "^4.15.4",
|
|
100
100
|
"typescript": "^7.0.2",
|
|
@@ -547,6 +547,21 @@ export interface Track {
|
|
|
547
547
|
* calls. Declare it on every live track.
|
|
548
548
|
*/
|
|
549
549
|
live?: boolean
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Byte encoding of this track's ICY `StreamTitle` when it is not UTF-8, as
|
|
553
|
+
* an IANA label (`'windows-1251'`, `'windows-874'`, …). Some Shoutcast /
|
|
554
|
+
* Icecast stations send titles in a legacy code page; ICY declares no
|
|
555
|
+
* encoding, so the player's UTF-8 decode fails and falls back to Latin-1,
|
|
556
|
+
* showing mojibake. With this set, a title that came out Latin-1 shaped
|
|
557
|
+
* (every char at or below U+00FF, some at or above U+0080) is re-decoded
|
|
558
|
+
* through ISO-8859-1 with the given charset; this also repairs stations
|
|
559
|
+
* that UTF-8 encoded their legacy bytes. A title already in another script
|
|
560
|
+
* or pure ASCII is left alone. Applies to ICY only: ID3 and HLS timed
|
|
561
|
+
* metadata declare their own encoding and are never touched. A label the
|
|
562
|
+
* platform cannot decode with is ignored.
|
|
563
|
+
*/
|
|
564
|
+
icyCharset?: string
|
|
550
565
|
}
|
|
551
566
|
|
|
552
567
|
/**
|
package/website/guide/track.md
CHANGED
|
@@ -149,6 +149,7 @@ Optional fields that change how a Track renders on CarPlay / Android Auto. Each
|
|
|
149
149
|
| `favorited` | filled/empty heart (needs the `favorite` capability) | Android Auto, notification |
|
|
150
150
|
| `live` | a "live" indicator | iOS now-playing |
|
|
151
151
|
| `albumPath` | make the now-playing album line tappable | CarPlay |
|
|
152
|
+
| `icyCharset` | decode ICY titles a station sends in a legacy code page | iOS, Android |
|
|
152
153
|
|
|
153
154
|
A couple of constraints worth knowing:
|
|
154
155
|
|