react-native-audio-browser 0.2.0-next.15 → 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.
Files changed (35) hide show
  1. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +3 -0
  2. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +4 -0
  3. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +1 -0
  4. package/android/src/main/java/com/audiobrowser/model/IcyTitle.kt +24 -0
  5. package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +10 -6
  6. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +2 -0
  7. package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +1 -1
  8. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +1 -0
  9. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +3 -0
  10. package/android/src/test/java/com/audiobrowser/model/IcyTitleTest.kt +49 -0
  11. package/ios/Browser/BrowserManager.swift +1 -0
  12. package/ios/Browser/JsonModels.swift +11 -0
  13. package/ios/CarPlay/CarPlayListItemFactory.swift +1 -1
  14. package/ios/Extension/ResolvedTrack+Copying.swift +2 -0
  15. package/ios/Extension/String+IcyCharset.swift +21 -0
  16. package/ios/Extension/TimedMetadata+AVFoundation.swift +6 -4
  17. package/ios/Extension/Track+Copying.swift +2 -0
  18. package/ios/HybridAudioBrowser.swift +1 -1
  19. package/ios/Model/NitroTypeStubs.swift +2 -0
  20. package/ios/Tests/PlaybackStateStoreTests.swift +1 -1
  21. package/ios/Tests/ResolvedTrackCopyingTests.swift +2 -0
  22. package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
  23. package/lib/typescript/src/types/browser-nodes.d.ts +14 -0
  24. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  25. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +7 -3
  26. package/nitrogen/generated/android/c++/JTrack.hpp +7 -3
  27. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +6 -3
  28. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -3
  29. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +19 -1
  30. package/nitrogen/generated/ios/swift/Track.swift +19 -1
  31. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +6 -2
  32. package/nitrogen/generated/shared/c++/Track.hpp +6 -2
  33. package/package.json +1 -1
  34. package/src/types/browser-nodes.ts +15 -0
  35. package/website/guide/track.md +1 -0
@@ -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
  }
@@ -44,4 +44,5 @@ fun ResolvedTrack.toTrack(): Track =
44
44
  disabled = disabled,
45
45
  favorited = favorited,
46
46
  live = live,
47
+ icyCharset = icyCharset,
47
48
  )
@@ -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
- if (entry.title != null) {
40
- return PlaybackMetadata(title = entry.title)
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)
@@ -64,6 +64,7 @@ object TrackFactory {
64
64
  disabled = null,
65
65
  favorited = null,
66
66
  live = null,
67
+ icyCharset = null,
67
68
  )
68
69
 
69
70
  fun toMedia3(tracks: Array<Track>): List<MediaItem> {
@@ -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
+ }
@@ -738,6 +738,7 @@ final class BrowserManager {
738
738
  disabled: nil,
739
739
  favorited: nil,
740
740
  live: nil,
741
+ icyCharset: nil,
741
742
  )
742
743
  }
743
744
 
@@ -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
  }
@@ -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
- static func from(items: [AVMetadataItem]) -> TimedMetadata? {
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 {
@@ -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.15",
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",
@@ -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
  /**
@@ -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