Skip to content

Commit 836f042

Browse files
committed
Fix the metadata provider selection for providers paired with streaming providers
1 parent 517c319 commit 836f042

File tree

6 files changed

+323
-234
lines changed

6 files changed

+323
-234
lines changed

packages/player/src/services/providersHost.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
initializeProvidersStore,
99
useProvidersStore,
1010
} from '../stores/providersStore';
11+
import { setupStreamingPairingSync } from './streamingPairingSync';
1112

1213
const createProvidersHost = (): ProvidersHost => {
1314
const byKind = new Map<ProviderKind, Map<string, ProviderDescriptor>>();
@@ -113,4 +114,6 @@ const createProvidersHost = (): ProvidersHost => {
113114

114115
export const providersHost: ProvidersHost = createProvidersHost();
115116

117+
setupStreamingPairingSync(providersHost);
118+
116119
void initializeProvidersStore();
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type {
2+
MetadataProvider,
3+
ProvidersHost,
4+
} from '@nuclearplayer/plugin-sdk';
5+
6+
import { useProvidersStore } from '../stores/providersStore';
7+
8+
export const setupStreamingPairingSync = (host: ProvidersHost): void => {
9+
let previousMetadataId: string | undefined;
10+
11+
useProvidersStore.subscribe((state) => {
12+
const currentMetadataId = state.active.metadata;
13+
14+
if (currentMetadataId !== previousMetadataId) {
15+
previousMetadataId = currentMetadataId;
16+
if (currentMetadataId) {
17+
const metadataProvider = host.get<MetadataProvider>(
18+
currentMetadataId,
19+
'metadata',
20+
);
21+
if (metadataProvider?.streamingProviderId) {
22+
host.setActive('streaming', metadataProvider.streamingProviderId);
23+
}
24+
}
25+
}
26+
});
27+
};

packages/player/src/test/fixtures/sources.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,36 @@
11
import type { SearchResults } from '@nuclearplayer/model';
2+
import type {
3+
MetadataProvider,
4+
StreamingProvider,
5+
} from '@nuclearplayer/plugin-sdk';
6+
7+
import { MetadataProviderBuilder } from '../builders/MetadataProviderBuilder';
8+
import { StreamingProviderBuilder } from '../builders/StreamingProviderBuilder';
9+
10+
export const PAIRED_STREAMING_PROVIDER: StreamingProvider =
11+
new StreamingProviderBuilder()
12+
.withId('beta-stream')
13+
.withName('Beta Stream')
14+
.build();
15+
16+
export const PAIRED_METADATA_PROVIDER: MetadataProvider =
17+
new MetadataProviderBuilder()
18+
.withId('beta-meta')
19+
.withName('Beta Search')
20+
.withStreamingProviderId('beta-stream')
21+
.build();
22+
23+
export const UNPAIRED_STREAMING_PROVIDER: StreamingProvider =
24+
new StreamingProviderBuilder()
25+
.withId('alpha-stream')
26+
.withName('Alpha Stream')
27+
.build();
28+
29+
export const UNPAIRED_METADATA_PROVIDER: MetadataProvider =
30+
new MetadataProviderBuilder()
31+
.withId('gamma-meta')
32+
.withName('Gamma Search')
33+
.build();
234

335
export const SEARCH_RESULTS_PROVIDER_A: SearchResults = {
436
artists: [
Lines changed: 105 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import { providersHost } from '../../services/providersHost';
2+
import { useProvidersStore } from '../../stores/providersStore';
23
import { DashboardProviderBuilder } from '../../test/builders/DashboardProviderBuilder';
34
import { DiscoveryProviderBuilder } from '../../test/builders/DiscoveryProviderBuilder';
45
import { MetadataProviderBuilder } from '../../test/builders/MetadataProviderBuilder';
56
import { PlaylistProviderBuilder } from '../../test/builders/PlaylistProviderBuilder';
67
import { StreamingProviderBuilder } from '../../test/builders/StreamingProviderBuilder';
8+
import {
9+
PAIRED_METADATA_PROVIDER,
10+
PAIRED_STREAMING_PROVIDER,
11+
UNPAIRED_METADATA_PROVIDER,
12+
UNPAIRED_STREAMING_PROVIDER,
13+
} from '../../test/fixtures/sources';
714
import { SourcesWrapper } from './Sources.test-wrapper';
815

916
describe('Sources view', () => {
@@ -41,13 +48,11 @@ describe('Sources view', () => {
4148
});
4249

4350
it('lists registered providers as options in the provider select', async () => {
44-
providersHost.register(
45-
new StreamingProviderBuilder().withId('yt').withName('YouTube').build(),
46-
);
51+
providersHost.register(UNPAIRED_STREAMING_PROVIDER);
4752
providersHost.register(
4853
new StreamingProviderBuilder()
49-
.withId('bc-stream')
50-
.withName('Bandcamp')
54+
.withId('beta-stream')
55+
.withName('Beta Stream')
5156
.build(),
5257
);
5358

@@ -57,125 +62,89 @@ describe('Sources view', () => {
5762
await SourcesWrapper.section(
5863
'streaming',
5964
).providerSelect.availableOptions(),
60-
).toEqual(['YouTube', 'Bandcamp']);
65+
).toEqual(['Alpha Stream', 'Beta Stream']);
6166
});
6267

6368
it('selects the first provider by default', async () => {
64-
providersHost.register(
65-
new StreamingProviderBuilder().withId('yt').withName('YouTube').build(),
66-
);
69+
providersHost.register(UNPAIRED_STREAMING_PROVIDER);
6770
providersHost.register(
6871
new StreamingProviderBuilder()
69-
.withId('bc-stream')
70-
.withName('Bandcamp')
72+
.withId('beta-stream')
73+
.withName('Beta Stream')
7174
.build(),
7275
);
7376

7477
await SourcesWrapper.mount();
7578

7679
expect(SourcesWrapper.section('streaming').providerSelect.selected()).toBe(
77-
'YouTube',
80+
'Alpha Stream',
7881
);
7982
});
8083

8184
it('switches the active provider when selecting another one', async () => {
82-
providersHost.register(
83-
new StreamingProviderBuilder().withId('yt').withName('YouTube').build(),
84-
);
85+
providersHost.register(UNPAIRED_STREAMING_PROVIDER);
8586
providersHost.register(
8687
new StreamingProviderBuilder()
87-
.withId('bc-stream')
88-
.withName('Bandcamp')
88+
.withId('beta-stream')
89+
.withName('Beta Stream')
8990
.build(),
9091
);
9192

9293
await SourcesWrapper.mount();
93-
await SourcesWrapper.section('streaming').providerSelect.select('Bandcamp');
94+
await SourcesWrapper.section('streaming').providerSelect.select(
95+
'Beta Stream',
96+
);
9497

9598
expect(SourcesWrapper.section('streaming').providerSelect.selected()).toBe(
96-
'Bandcamp',
99+
'Beta Stream',
97100
);
98101
});
99102

100103
it('locks the streaming select when metadata provider declares streamingProviderId', async () => {
101-
providersHost.register(
102-
new StreamingProviderBuilder()
103-
.withId('bc-stream')
104-
.withName('Bandcamp Stream')
105-
.build(),
106-
);
107-
providersHost.register(
108-
new StreamingProviderBuilder().withId('yt').withName('YouTube').build(),
109-
);
110-
providersHost.register(
111-
new MetadataProviderBuilder()
112-
.withId('bc-meta')
113-
.withName('Bandcamp')
114-
.withStreamingProviderId('bc-stream')
115-
.build(),
116-
);
104+
providersHost.register(PAIRED_STREAMING_PROVIDER);
105+
providersHost.register(UNPAIRED_STREAMING_PROVIDER);
106+
providersHost.register(PAIRED_METADATA_PROVIDER);
117107

118108
await SourcesWrapper.mount();
119-
await SourcesWrapper.section('metadata').providerSelect.select('Bandcamp');
109+
await SourcesWrapper.section('metadata').providerSelect.select(
110+
'Beta Search',
111+
);
120112

121113
expect(SourcesWrapper.section('streaming').providerSelect.selected()).toBe(
122-
'Bandcamp Stream',
114+
'Beta Stream',
123115
);
124116
expect(
125117
SourcesWrapper.section('streaming').providerSelect.isDisabled(),
126118
).toBe(true);
127119
});
128120

129121
it('explains why the streaming select is locked', async () => {
130-
providersHost.register(
131-
new StreamingProviderBuilder()
132-
.withId('bc-stream')
133-
.withName('Bandcamp Stream')
134-
.build(),
135-
);
136-
providersHost.register(
137-
new MetadataProviderBuilder()
138-
.withId('bc-meta')
139-
.withName('Bandcamp')
140-
.withStreamingProviderId('bc-stream')
141-
.build(),
142-
);
122+
providersHost.register(PAIRED_STREAMING_PROVIDER);
123+
providersHost.register(PAIRED_METADATA_PROVIDER);
143124

144125
await SourcesWrapper.mount();
145-
await SourcesWrapper.section('metadata').providerSelect.select('Bandcamp');
126+
await SourcesWrapper.section('metadata').providerSelect.select(
127+
'Beta Search',
128+
);
146129

147130
expect(SourcesWrapper.section('streaming').lockedReason).toHaveTextContent(
148-
'Bandcamp metadata source requires Bandcamp Stream streaming source.',
131+
'Beta Search metadata source requires Beta Stream streaming source.',
149132
);
150133
});
151134

152135
it('unlocks the streaming select when switching to a metadata provider without streamingProviderId', async () => {
153-
providersHost.register(
154-
new StreamingProviderBuilder()
155-
.withId('bc-stream')
156-
.withName('Bandcamp Stream')
157-
.build(),
158-
);
159-
providersHost.register(
160-
new StreamingProviderBuilder().withId('yt').withName('YouTube').build(),
161-
);
162-
providersHost.register(
163-
new MetadataProviderBuilder()
164-
.withId('bc-meta')
165-
.withName('Bandcamp')
166-
.withStreamingProviderId('bc-stream')
167-
.build(),
168-
);
169-
providersHost.register(
170-
new MetadataProviderBuilder()
171-
.withId('lastfm')
172-
.withName('Last.fm')
173-
.build(),
174-
);
136+
providersHost.register(PAIRED_STREAMING_PROVIDER);
137+
providersHost.register(UNPAIRED_STREAMING_PROVIDER);
138+
providersHost.register(PAIRED_METADATA_PROVIDER);
139+
providersHost.register(UNPAIRED_METADATA_PROVIDER);
175140

176141
await SourcesWrapper.mount();
177-
await SourcesWrapper.section('metadata').providerSelect.select('Bandcamp');
178-
await SourcesWrapper.section('metadata').providerSelect.select('Last.fm');
142+
await SourcesWrapper.section('metadata').providerSelect.select(
143+
'Beta Search',
144+
);
145+
await SourcesWrapper.section('metadata').providerSelect.select(
146+
'Gamma Search',
147+
);
179148

180149
expect(
181150
SourcesWrapper.section('streaming').providerSelect.isDisabled(),
@@ -185,32 +154,80 @@ describe('Sources view', () => {
185154
it('shows a warning when metadata provider requires a streaming provider that is not registered', async () => {
186155
providersHost.register(
187156
new MetadataProviderBuilder()
188-
.withId('bc-meta')
189-
.withName('Bandcamp')
190-
.withStreamingProviderId('bc-stream')
157+
.withId('beta-meta')
158+
.withName('Beta Search')
159+
.withStreamingProviderId('beta-stream')
191160
.build(),
192161
);
193162

194163
await SourcesWrapper.mount();
195164

196165
expect(
197-
SourcesWrapper.section('metadata').provider('Bandcamp').warning,
166+
SourcesWrapper.section('metadata').provider('Beta Search').warning,
198167
).toHaveTextContent(
199-
'Required streaming provider "bc-stream" is not available',
168+
'Required streaming provider "beta-stream" is not available',
169+
);
170+
});
171+
172+
it('syncs the streaming provider in the store when selecting a metadata provider with a locked pair', async () => {
173+
providersHost.register(UNPAIRED_STREAMING_PROVIDER);
174+
providersHost.register(PAIRED_STREAMING_PROVIDER);
175+
providersHost.register(UNPAIRED_METADATA_PROVIDER);
176+
providersHost.register(PAIRED_METADATA_PROVIDER);
177+
178+
await SourcesWrapper.mount();
179+
await SourcesWrapper.section('metadata').providerSelect.select(
180+
'Beta Search',
200181
);
182+
183+
expect(useProvidersStore.getState().active.streaming).toBe('beta-stream');
184+
});
185+
186+
it('keeps the streaming provider when switching to a metadata provider without a locked pair', async () => {
187+
providersHost.register(PAIRED_STREAMING_PROVIDER);
188+
providersHost.register(UNPAIRED_STREAMING_PROVIDER);
189+
providersHost.register(PAIRED_METADATA_PROVIDER);
190+
providersHost.register(UNPAIRED_METADATA_PROVIDER);
191+
192+
await SourcesWrapper.mount();
193+
await SourcesWrapper.section('metadata').providerSelect.select(
194+
'Beta Search',
195+
);
196+
await SourcesWrapper.section('metadata').providerSelect.select(
197+
'Gamma Search',
198+
);
199+
200+
expect(useProvidersStore.getState().active.streaming).toBe('beta-stream');
201+
});
202+
203+
it('syncs the streaming provider on initial mount when the first metadata provider has a locked pair', async () => {
204+
providersHost.register(PAIRED_STREAMING_PROVIDER);
205+
providersHost.register(PAIRED_METADATA_PROVIDER);
206+
207+
await SourcesWrapper.mount();
208+
209+
expect(useProvidersStore.getState().active.streaming).toBe('beta-stream');
210+
});
211+
212+
it('activates the paired streaming provider when the active metadata provider has a locked pair on startup', async () => {
213+
providersHost.register(PAIRED_STREAMING_PROVIDER);
214+
providersHost.register(PAIRED_METADATA_PROVIDER);
215+
216+
// No mount() - the sync happens at the store level, not in React
217+
expect(useProvidersStore.getState().active.streaming).toBe('beta-stream');
201218
});
202219

203220
it('lists and switches discovery providers', async () => {
204221
providersHost.register(
205222
new DiscoveryProviderBuilder()
206-
.withId('disco-a')
207-
.withName('Discovery A')
223+
.withId('delta-disco')
224+
.withName('Delta Discovery')
208225
.build(),
209226
);
210227
providersHost.register(
211228
new DiscoveryProviderBuilder()
212-
.withId('disco-b')
213-
.withName('Discovery B')
229+
.withId('epsilon-disco')
230+
.withName('Epsilon Discovery')
214231
.build(),
215232
);
216233

@@ -220,14 +237,14 @@ describe('Sources view', () => {
220237
await SourcesWrapper.section(
221238
'discovery',
222239
).providerSelect.availableOptions(),
223-
).toEqual(['Discovery A', 'Discovery B']);
240+
).toEqual(['Delta Discovery', 'Epsilon Discovery']);
224241

225242
await SourcesWrapper.section('discovery').providerSelect.select(
226-
'Discovery B',
243+
'Epsilon Discovery',
227244
);
228245

229246
expect(SourcesWrapper.section('discovery').providerSelect.selected()).toBe(
230-
'Discovery B',
247+
'Epsilon Discovery',
231248
);
232249
});
233250
});

0 commit comments

Comments
 (0)