react-native-audio-browser 0.2.0-next.31 → 0.2.0-next.33
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/ios/AVPlayerEngine.swift +4 -8
- package/ios/Browser/BrowserConfig.swift +0 -5
- package/ios/Browser/BrowserError.swift +16 -0
- package/ios/Browser/BrowserManager.swift +36 -141
- package/ios/Browser/BrowserPathHelper.swift +25 -12
- package/ios/Browser/FavoriteSet.swift +46 -0
- package/ios/Browser/JsonModels.swift +172 -381
- package/ios/Browser/PageResolver.swift +16 -96
- package/ios/Browser/RequestLayer.swift +97 -2
- package/ios/Browser/RequestPipeline.swift +78 -227
- package/ios/Browser/SectionScope.swift +25 -0
- package/ios/Browser/SimpleRouter.swift +73 -144
- package/ios/Extension/SearchParams+Query.swift +28 -0
- package/ios/Extension/Track+Copying.swift +3 -1
- package/ios/Extension/Track+Identity.swift +7 -0
- package/ios/Http/HttpClient.swift +12 -30
- package/ios/HybridAudioBrowser+Browser.swift +5 -21
- package/ios/HybridAudioBrowser+Player.swift +3 -3
- package/ios/Model/NitroTypeStubs.swift +78 -0
- package/ios/Player/AudioSessionController.swift +2 -0
- package/ios/Player/ErrorStatus.swift +25 -2
- package/ios/Player/MediaLoader.swift +23 -71
- package/ios/Player/NowPlayingLines.swift +36 -0
- package/ios/Player/NowPlayingUpdater.swift +27 -85
- package/ios/Player/Player.swift +108 -261
- package/ios/Player/RetryLoop.swift +26 -0
- package/ios/Player/SleepTimerManager.swift +13 -15
- package/ios/Player/VolumeFader.swift +5 -0
- package/ios/Tests/BrowserPathHelperTests.swift +33 -0
- package/ios/Tests/ErrorStatusTests.swift +50 -0
- package/ios/Tests/FavoriteSetTests.swift +60 -0
- package/ios/Tests/JsonModelsTests.swift +30 -0
- package/ios/Tests/MediaLoaderTests.swift +26 -21
- package/ios/Tests/NowPlayingLinesTests.swift +43 -0
- package/ios/Tests/PlayerPolicyTests.swift +19 -3
- package/ios/Tests/PlayerTests.swift +81 -74
- package/ios/Tests/RequestLayerTests.swift +116 -0
- package/ios/Tests/RetryLoopTests.swift +35 -0
- package/ios/Tests/SearchParamsQueryTests.swift +48 -0
- package/ios/Tests/SectionScopeTests.swift +53 -0
- package/ios/Tests/SimpleRouterTests.swift +38 -3
- package/ios/Tests/SleepTimerManagerTests.swift +65 -41
- package/ios/Tests/Support/TestTypes.swift +2 -2
- package/package.json +1 -1
|
@@ -111,4 +111,57 @@ struct SectionScopeTests {
|
|
|
111
111
|
@Test func childlessPageHasNoSections() {
|
|
112
112
|
#expect(ResolvedTrack(path: "/p", title: "P").normalizedSections == nil)
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
// MARK: - Queue
|
|
116
|
+
|
|
117
|
+
private func queue(_ sections: [Section], _ path: String, singleTrack: Bool = false) -> ([String?], Int)? {
|
|
118
|
+
SectionScope.queue(in: sections, path: path, singleTrack: singleTrack).map { ($0.tracks.map(\.src), $0.selectedIndex) }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@Test func queuesThePlayableTracksOfTheTappedSection() {
|
|
122
|
+
let page = [
|
|
123
|
+
section("First", [track("a"), Track(id: "off", src: "off", disabled: true), Track(path: "/browse"), track("b")]),
|
|
124
|
+
section("Second", [track("c")]),
|
|
125
|
+
]
|
|
126
|
+
let result = queue(page, "/p?__trackId=b&__index=3")
|
|
127
|
+
#expect(result?.0 == ["a", "b"])
|
|
128
|
+
#expect(result?.1 == 1)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@Test func thePinnedCopyIsTheSelectedOne() {
|
|
132
|
+
let page = [section(nil, [track("a"), track("b"), track("a")])]
|
|
133
|
+
#expect(queue(page, "/p?__trackId=a&__index=2")?.1 == 2)
|
|
134
|
+
#expect(queue(page, "/p?__trackId=a")?.1 == 0)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@Test func aStaleIndexSelectsTheFirstIdentityMatch() {
|
|
138
|
+
let page = [section(nil, [track("x"), track("a"), track("a")])]
|
|
139
|
+
#expect(queue(page, "/p?__trackId=a&__index=0")?.1 == 1)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@Test func singleTrackQueuesOnlyTheTappedTrack() {
|
|
143
|
+
let page = [section(nil, [track("a"), track("b"), track("c")])]
|
|
144
|
+
let result = queue(page, "/p?__trackId=b&__index=1", singleTrack: true)
|
|
145
|
+
#expect(result?.0 == ["b"])
|
|
146
|
+
#expect(result?.1 == 0)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@Test func aDisabledTappedTrackQueuesNothing() {
|
|
150
|
+
let page = [section(nil, [track("a"), Track(id: "off", src: "off", disabled: true)])]
|
|
151
|
+
#expect(queue(page, "/p?__trackId=off&__index=1") == nil)
|
|
152
|
+
#expect(queue(page, "/p?__trackId=off") == nil)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Test func aVanishedIdentityOrAPlainPathQueuesNothing() {
|
|
156
|
+
let page = [section(nil, [track("a")])]
|
|
157
|
+
#expect(queue(page, "/p?__trackId=gone") == nil)
|
|
158
|
+
#expect(queue(page, "/p") == nil)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@Test func isPlayableNeedsASrcAndNoDisabled() {
|
|
162
|
+
#expect(Track(src: "s").isPlayable)
|
|
163
|
+
#expect(Track(src: "s", disabled: false).isPlayable)
|
|
164
|
+
#expect(!Track(src: "s", disabled: true).isPlayable)
|
|
165
|
+
#expect(!Track(path: "/p").isPlayable)
|
|
166
|
+
}
|
|
114
167
|
}
|
|
@@ -7,9 +7,7 @@ private struct TestRoute: RouteEntry {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
private func match(_ path: String, _ patterns: [String]) -> (route: TestRoute, match: RouteMatch)? {
|
|
10
|
-
|
|
11
|
-
let routes = patterns.map { TestRoute(path: $0) }
|
|
12
|
-
return router.findBestMatch(path: path, routes: routes)
|
|
10
|
+
SimpleRouter.findBestMatch(path: path, routes: patterns.map { TestRoute(path: $0) })
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
// MARK: - exact matches
|
|
@@ -137,3 +135,40 @@ private func match(_ path: String, _ patterns: [String]) -> (route: TestRoute, m
|
|
|
137
135
|
let result = match("/api/v1/users", ["/api/{version}/**", "/api/v1/**"])
|
|
138
136
|
#expect(result!.route.path == "/api/v1/**")
|
|
139
137
|
}
|
|
138
|
+
|
|
139
|
+
@Test func treatsADoubleStarBeforeTheEndAsAConstant() {
|
|
140
|
+
#expect(match("/a/**/b", ["/a/**/b"]) != nil)
|
|
141
|
+
#expect(match("/a/x/b", ["/a/**/b"]) == nil)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@Test func theFirstRouteWinsATie() {
|
|
145
|
+
let result = match("/artists/123", ["/artists/{id}", "/artists/{name}"])
|
|
146
|
+
#expect(result!.route.path == "/artists/{id}")
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// MARK: - browse route
|
|
150
|
+
|
|
151
|
+
private func browseRoute(_ path: String, _ patterns: [String]) -> (route: TestRoute, match: RouteMatch)? {
|
|
152
|
+
SimpleRouter.browseRoute(for: path, in: patterns.map { TestRoute(path: $0) })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Test func theSpecialRoutesNeverMatchABrowsePath() {
|
|
156
|
+
#expect(browseRoute("/__tabs__", ["__tabs__", "__search__"]) == nil)
|
|
157
|
+
#expect(browseRoute("__search__", ["__search__"]) == nil)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@Test func aMatchingRouteWinsOverTheDefault() {
|
|
161
|
+
let result = browseRoute("/artists/1", ["__default__", "/artists/{id}"])
|
|
162
|
+
#expect(result!.route.path == "/artists/{id}")
|
|
163
|
+
#expect(result!.match.params == ["id": "1"])
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@Test func withoutAMatchTheDefaultRouteBrowsesThePath() {
|
|
167
|
+
let result = browseRoute("/a/b", ["/artists", "__default__"])
|
|
168
|
+
#expect(result!.route.path == "__default__")
|
|
169
|
+
#expect(result!.match.params == ["path": "/a/b"])
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@Test func withoutAMatchOrADefaultThereIsNoBrowseRoute() {
|
|
173
|
+
#expect(browseRoute("/a/b", ["/artists", "__tabs__"]) == nil)
|
|
174
|
+
}
|
|
@@ -5,103 +5,127 @@ import Testing
|
|
|
5
5
|
// Timing tests drive a ManualSleepTimerScheduler rather than the main queue:
|
|
6
6
|
// every deadline below is virtual, so nothing races the real clock.
|
|
7
7
|
|
|
8
|
+
@MainActor
|
|
9
|
+
private final class Volume {
|
|
10
|
+
var level: Float = 0.8
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@MainActor
|
|
14
|
+
private func makeManager() -> (SleepTimerManager, ManualSleepTimerScheduler, Volume, VolumeFader) {
|
|
15
|
+
let clock = ManualSleepTimerScheduler()
|
|
16
|
+
let volume = Volume()
|
|
17
|
+
let fader = VolumeFader(getVolume: { volume.level }, setVolume: { volume.level = $0 })
|
|
18
|
+
return (SleepTimerManager(fader: fader, scheduler: clock), clock, volume, fader)
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
@Suite("SleepTimerManager - fade scheduling")
|
|
9
22
|
struct SleepTimerManagerTests {
|
|
10
23
|
@Test @MainActor
|
|
11
|
-
func
|
|
12
|
-
let clock =
|
|
13
|
-
let manager = SleepTimerManager(scheduler: clock)
|
|
14
|
-
var fadeStarts: [TimeInterval] = []
|
|
24
|
+
func fadeStartsBeforeDeadline_completionRestoresAfterHalting() {
|
|
25
|
+
let (manager, clock, volume, fader) = makeManager()
|
|
15
26
|
var completes = 0
|
|
16
|
-
var
|
|
17
|
-
manager.
|
|
18
|
-
|
|
27
|
+
var volumeAtHalt: Float?
|
|
28
|
+
manager.onComplete = {
|
|
29
|
+
completes += 1
|
|
30
|
+
volumeAtHalt = volume.level
|
|
31
|
+
}
|
|
19
32
|
manager.set(seconds: 0.3, fadeDuration: 0.15)
|
|
20
|
-
// Attach after set() — replacing a timer emits a defensive onFadeCancel.
|
|
21
|
-
manager.onFadeCancel = { cancels += 1 }
|
|
22
33
|
|
|
23
34
|
clock.advance(to: 0.05) // before the fade window
|
|
24
|
-
#expect(
|
|
35
|
+
#expect(!manager.isFading)
|
|
25
36
|
#expect(manager.get() != nil)
|
|
26
37
|
|
|
27
38
|
clock.advance(to: 0.22) // inside the fade window
|
|
28
|
-
#expect(
|
|
39
|
+
#expect(manager.isFading)
|
|
40
|
+
#expect(fader.duration == 0.15)
|
|
41
|
+
#expect(fader.originalVolume == 0.8)
|
|
29
42
|
#expect(completes == 0)
|
|
43
|
+
volume.level = 0.1 // mid-fade
|
|
30
44
|
|
|
31
45
|
clock.advance(to: 0.4) // past the deadline
|
|
32
46
|
#expect(completes == 1)
|
|
33
|
-
#expect(
|
|
47
|
+
#expect(volumeAtHalt == 0.1) // halted before the restore
|
|
48
|
+
#expect(volume.level == 0.8)
|
|
49
|
+
#expect(!manager.isFading)
|
|
34
50
|
#expect(manager.get() == nil)
|
|
35
51
|
}
|
|
36
52
|
|
|
37
53
|
@Test @MainActor
|
|
38
54
|
func fadeLongerThanTimer_clampsAndStartsImmediately() {
|
|
39
|
-
let clock =
|
|
40
|
-
let manager = SleepTimerManager(scheduler: clock)
|
|
41
|
-
var fadeStarts: [TimeInterval] = []
|
|
42
|
-
manager.onFadeStart = { fadeStarts.append($0) }
|
|
55
|
+
let (manager, clock, _, fader) = makeManager()
|
|
43
56
|
|
|
44
57
|
manager.set(seconds: 0.2, fadeDuration: 5)
|
|
45
58
|
|
|
46
59
|
clock.advance(to: 0.1) // fade should have begun
|
|
47
|
-
#expect(
|
|
60
|
+
#expect(manager.isFading)
|
|
61
|
+
#expect(fader.duration == 0.2)
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
@Test @MainActor
|
|
51
|
-
func
|
|
52
|
-
let clock =
|
|
53
|
-
let manager = SleepTimerManager(scheduler: clock)
|
|
54
|
-
var fadeStarts = 0
|
|
65
|
+
func clear_restoresVolume_andCancelsScheduledFade() {
|
|
66
|
+
let (manager, clock, volume, _) = makeManager()
|
|
55
67
|
manager.set(seconds: 0.2, fadeDuration: 0.15)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
manager.onFadeCancel = { cancels += 1 }
|
|
68
|
+
clock.advance(to: 0.1)
|
|
69
|
+
volume.level = 0.1 // mid-fade
|
|
59
70
|
|
|
60
71
|
#expect(manager.clear())
|
|
61
|
-
#expect(
|
|
72
|
+
#expect(volume.level == 0.8)
|
|
73
|
+
#expect(!manager.isFading)
|
|
62
74
|
|
|
63
75
|
clock.advance(to: 0.3) // well past the would-be deadline
|
|
64
|
-
#expect(
|
|
76
|
+
#expect(!manager.isFading)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Test @MainActor
|
|
80
|
+
func clearBeforeFade_cancelsScheduledFade() {
|
|
81
|
+
let (manager, clock, _, _) = makeManager()
|
|
82
|
+
manager.set(seconds: 0.2, fadeDuration: 0.15)
|
|
83
|
+
|
|
84
|
+
#expect(manager.clear())
|
|
85
|
+
|
|
86
|
+
clock.advance(to: 0.3)
|
|
87
|
+
#expect(!manager.isFading)
|
|
65
88
|
}
|
|
66
89
|
|
|
67
90
|
@Test @MainActor
|
|
68
|
-
func
|
|
69
|
-
let manager =
|
|
91
|
+
func replacingTimer_cancelsTheFade() {
|
|
92
|
+
let (manager, clock, volume, _) = makeManager()
|
|
70
93
|
manager.set(seconds: 60, fadeDuration: 10)
|
|
71
|
-
|
|
72
|
-
|
|
94
|
+
clock.advance(to: 55)
|
|
95
|
+
volume.level = 0.1 // mid-fade
|
|
73
96
|
|
|
74
97
|
manager.set(seconds: 30)
|
|
75
98
|
|
|
76
|
-
#expect(
|
|
99
|
+
#expect(!manager.isFading)
|
|
100
|
+
#expect(volume.level == 0.8)
|
|
77
101
|
}
|
|
78
102
|
|
|
79
103
|
@Test @MainActor
|
|
80
|
-
func
|
|
81
|
-
let manager =
|
|
104
|
+
func switchingToEndOfTrack_cancelsTheFade() {
|
|
105
|
+
let (manager, clock, volume, _) = makeManager()
|
|
82
106
|
manager.set(seconds: 60, fadeDuration: 10)
|
|
83
|
-
|
|
84
|
-
|
|
107
|
+
clock.advance(to: 55)
|
|
108
|
+
volume.level = 0.1 // mid-fade
|
|
85
109
|
|
|
86
110
|
manager.setToEndOfTrack()
|
|
87
111
|
|
|
88
|
-
#expect(
|
|
112
|
+
#expect(!manager.isFading)
|
|
113
|
+
#expect(volume.level == 0.8)
|
|
89
114
|
#expect(manager.get() == .third(SleepTimerEndOfTrack(sleepWhenPlayedToEnd: true)))
|
|
90
115
|
}
|
|
91
116
|
|
|
92
117
|
@Test @MainActor
|
|
93
118
|
func noFadeDuration_schedulesNoFade() {
|
|
94
|
-
let clock =
|
|
95
|
-
let manager = SleepTimerManager(scheduler: clock)
|
|
96
|
-
var fadeStarts = 0
|
|
119
|
+
let (manager, clock, volume, _) = makeManager()
|
|
97
120
|
var completes = 0
|
|
98
|
-
manager.onFadeStart = { _ in fadeStarts += 1 }
|
|
99
121
|
manager.onComplete = { completes += 1 }
|
|
100
122
|
|
|
101
123
|
manager.set(seconds: 0.1)
|
|
102
124
|
|
|
125
|
+
clock.advance(to: 0.05)
|
|
126
|
+
#expect(!manager.isFading)
|
|
103
127
|
clock.advance(to: 0.2)
|
|
104
|
-
#expect(fadeStarts == 0)
|
|
105
128
|
#expect(completes == 1)
|
|
129
|
+
#expect(volume.level == 0.8)
|
|
106
130
|
}
|
|
107
131
|
}
|
|
@@ -118,11 +118,11 @@ func makePlayer(
|
|
|
118
118
|
engine: MockPlayerEngine = MockPlayerEngine(),
|
|
119
119
|
retry: RetryLoop = makeRetryLoop(),
|
|
120
120
|
retries: Bool = true,
|
|
121
|
-
|
|
121
|
+
sleepTimerScheduler: any SleepTimerScheduling = ManualSleepTimerScheduler(),
|
|
122
122
|
saveInterval: Duration = .seconds(5),
|
|
123
123
|
) -> Player {
|
|
124
124
|
let player = Player(
|
|
125
|
-
engine: engine, retry: retry,
|
|
125
|
+
engine: engine, retry: retry, sleepTimerScheduler: sleepTimerScheduler,
|
|
126
126
|
persistenceDefaults: UserDefaults(suiteName: "PlayerTests")!, persistenceSaveInterval: saveInterval,
|
|
127
127
|
)
|
|
128
128
|
retry.shouldRetry = { retries }
|