Skip to content

Commit 0f34feb

Browse files
authored
Merge pull request #2000 from nukeop/feat/streaming-provider-v2
Introduce v2 methods for streaming providers
2 parents 942a112 + 1152cdd commit 0f34feb

File tree

6 files changed

+200
-22
lines changed

6 files changed

+200
-22
lines changed

packages/player/src/services/streamingHost.test.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,88 @@ describe('streamingHost', () => {
8989
);
9090
});
9191

92+
it('prefers searchForTrackV2 over searchForTrack when available', async () => {
93+
const mockCandidates = [
94+
{
95+
id: 'candidate-1',
96+
title: 'Test Track',
97+
source: { provider: 'test', id: 'vid1' },
98+
failed: false,
99+
},
100+
];
101+
102+
const searchForTrack = vi.fn();
103+
const searchForTrackV2 = vi.fn().mockResolvedValue(mockCandidates);
104+
105+
const provider: StreamingProvider = {
106+
id: 'test',
107+
kind: 'streaming',
108+
name: 'Test',
109+
searchForTrack,
110+
searchForTrackV2,
111+
getStreamUrl: vi.fn(),
112+
};
113+
114+
providersHost.register(provider);
115+
116+
const track: Track = {
117+
title: 'Test Track',
118+
artists: [{ name: 'Test Artist', roles: [] }],
119+
source: { provider: 'test', id: 'track-1' },
120+
};
121+
122+
const result = await streamingHost.resolveCandidatesForTrack(track);
123+
124+
expect(result.success).toBe(true);
125+
if (result.success) {
126+
expect(result.candidates).toEqual(mockCandidates);
127+
}
128+
expect(searchForTrackV2).toHaveBeenCalledWith(track);
129+
expect(searchForTrack).not.toHaveBeenCalled();
130+
});
131+
132+
it('falls back to searchForTrack when searchForTrackV2 is not defined', async () => {
133+
const mockCandidates = [
134+
{
135+
id: 'candidate-1',
136+
title: 'Test Track',
137+
source: { provider: 'test', id: 'vid1' },
138+
failed: false,
139+
},
140+
];
141+
142+
const searchForTrack = vi.fn().mockResolvedValue(mockCandidates);
143+
144+
const provider: StreamingProvider = {
145+
id: 'test',
146+
kind: 'streaming',
147+
name: 'Test',
148+
searchForTrack,
149+
getStreamUrl: vi.fn(),
150+
};
151+
152+
providersHost.register(provider);
153+
154+
const track: Track = {
155+
title: 'Test Track',
156+
artists: [{ name: 'Test Artist', roles: [] }],
157+
album: {
158+
title: 'Test Album',
159+
source: { provider: 'test', id: 'alb1' },
160+
},
161+
source: { provider: 'test', id: 'track-1' },
162+
};
163+
164+
const result = await streamingHost.resolveCandidatesForTrack(track);
165+
166+
expect(result.success).toBe(true);
167+
expect(searchForTrack).toHaveBeenCalledWith(
168+
'Test Artist',
169+
'Test Track',
170+
'Test Album',
171+
);
172+
});
173+
92174
it('handles search errors', async () => {
93175
const searchForTrack = vi
94176
.fn()
@@ -226,6 +308,41 @@ describe('streamingHost', () => {
226308
});
227309
});
228310

311+
it('prefers getStreamUrlV2 over getStreamUrl when available', async () => {
312+
const mockStream = {
313+
url: 'https://example.com/stream.mp3',
314+
protocol: 'https' as const,
315+
source: { provider: 'test', id: 'vid1' },
316+
};
317+
318+
const getStreamUrl = vi.fn();
319+
const getStreamUrlV2 = vi.fn().mockResolvedValue(mockStream);
320+
321+
const provider: StreamingProvider = {
322+
id: 'test',
323+
kind: 'streaming',
324+
name: 'Test',
325+
searchForTrack: vi.fn(),
326+
getStreamUrl,
327+
getStreamUrlV2,
328+
};
329+
330+
providersHost.register(provider);
331+
332+
const candidate: StreamCandidate = {
333+
id: 'candidate-1',
334+
title: 'Test Track',
335+
source: { provider: 'test', id: 'vid1' },
336+
failed: false,
337+
};
338+
339+
const result = await streamingHost.resolveStreamForCandidate(candidate);
340+
341+
expect(getStreamUrlV2).toHaveBeenCalledWith(candidate);
342+
expect(getStreamUrl).not.toHaveBeenCalled();
343+
expect(result!.stream).toEqual(mockStream);
344+
});
345+
229346
it('retries on failure and returns failed candidate after max retries', async () => {
230347
const getStreamUrl = vi
231348
.fn()

packages/player/src/services/streamingHost.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,14 @@ export const createStreamingHost = (): StreamingHost => ({
6262
}
6363

6464
try {
65-
const artistName = track.artists[0]?.name ?? 'Unknown Artist';
66-
const albumName = track.album?.title;
67-
68-
const candidates = await provider.searchForTrack(
69-
artistName,
70-
track.title,
71-
albumName,
72-
);
65+
// Temporarily provide both methods to avoid breaking existing providers until we have autoupdate for plugins
66+
const candidates = provider.searchForTrackV2
67+
? await provider.searchForTrackV2(track)
68+
: await provider.searchForTrack(
69+
track.artists[0]?.name ?? 'Unknown Artist',
70+
track.title,
71+
track.album?.title,
72+
);
7373

7474
return {
7575
success: true,
@@ -110,8 +110,12 @@ export const createStreamingHost = (): StreamingHost => ({
110110
.getValue('playback.streamResolutionRetries') as number) ?? 3;
111111

112112
try {
113+
// Temporarily provide both methods to avoid breaking existing providers until we have autoupdate for plugins
113114
const stream = await withRetry(
114-
() => provider.getStreamUrl(candidate.id),
115+
() =>
116+
provider.getStreamUrlV2
117+
? provider.getStreamUrlV2(candidate)
118+
: provider.getStreamUrl(candidate.id),
115119
retries,
116120
);
117121

packages/player/src/services/streamingPairingSync.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,34 @@ import type {
55

66
import { useProvidersStore } from '../stores/providersStore';
77

8+
const tryPairStreaming = (host: ProvidersHost): void => {
9+
const currentMetadataId = useProvidersStore.getState().active.metadata;
10+
if (!currentMetadataId) {
11+
return;
12+
}
13+
14+
const metadataProvider = host.get<MetadataProvider>(
15+
currentMetadataId,
16+
'metadata',
17+
);
18+
if (!metadataProvider?.streamingProviderId) {
19+
return;
20+
}
21+
22+
const currentStreamingId = useProvidersStore.getState().active.streaming;
23+
if (currentStreamingId === metadataProvider.streamingProviderId) {
24+
return;
25+
}
26+
27+
const streamingProvider = host.get(
28+
metadataProvider.streamingProviderId,
29+
'streaming',
30+
);
31+
if (streamingProvider) {
32+
host.setActive('streaming', metadataProvider.streamingProviderId);
33+
}
34+
};
35+
836
export const setupStreamingPairingSync = (host: ProvidersHost): void => {
937
let previousMetadataId: string | undefined;
1038

@@ -13,15 +41,11 @@ export const setupStreamingPairingSync = (host: ProvidersHost): void => {
1341

1442
if (currentMetadataId !== previousMetadataId) {
1543
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-
}
44+
tryPairStreaming(host);
2545
}
2646
});
47+
48+
host.subscribe(() => {
49+
tryPairStreaming(host);
50+
});
2751
};

packages/player/src/views/Sources/Sources.test.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,27 @@ describe('Sources view', () => {
217217
expect(useProvidersStore.getState().active.streaming).toBe('beta-stream');
218218
});
219219

220+
it('activates the paired streaming provider when it registers after the metadata provider', async () => {
221+
providersHost.register(PAIRED_METADATA_PROVIDER);
222+
providersHost.register(PAIRED_STREAMING_PROVIDER);
223+
224+
expect(useProvidersStore.getState().active.streaming).toBe('beta-stream');
225+
});
226+
227+
it('does not lock the streaming select before the paired streaming provider registers', async () => {
228+
providersHost.register(UNPAIRED_STREAMING_PROVIDER);
229+
providersHost.register(PAIRED_METADATA_PROVIDER);
230+
231+
await SourcesWrapper.mount();
232+
233+
expect(
234+
SourcesWrapper.section('streaming').providerSelect.isDisabled(),
235+
).toBe(false);
236+
expect(SourcesWrapper.section('streaming').providerSelect.selected()).toBe(
237+
'Alpha Stream',
238+
);
239+
});
240+
220241
it('lists and switches discovery providers', async () => {
221242
providersHost.register(
222243
new DiscoveryProviderBuilder()

packages/player/src/views/Sources/hooks/useStreamingPairing.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,22 @@ export const useStreamingPairing = (
1515
activeMetadata: MetadataProvider | undefined,
1616
streamingProviders: ProviderDescriptor<'streaming'>[],
1717
): StreamingPairing => {
18+
// We check if the paired streaming provider for this metadata provider is
19+
// available, and return it.
20+
// This is a display-only match. The streaming provider gets activated for real here:
21+
// packages/player/src/services/streamingPairingSync.ts
1822
return useMemo(() => {
19-
const lockedStreamingId = activeMetadata?.streamingProviderId;
20-
const lockedStreamingProvider = lockedStreamingId
21-
? streamingProviders.find((provider) => provider.id === lockedStreamingId)
23+
const desiredStreamingId = activeMetadata?.streamingProviderId;
24+
const lockedStreamingProvider = desiredStreamingId
25+
? streamingProviders.find(
26+
(provider) => provider.id === desiredStreamingId,
27+
)
2228
: undefined;
2329

2430
return {
25-
lockedStreamingId,
31+
lockedStreamingId: lockedStreamingProvider
32+
? desiredStreamingId
33+
: undefined,
2634
metadataName: activeMetadata?.name,
2735
streamingName: lockedStreamingProvider?.name,
2836
};

packages/plugin-sdk/src/types/streaming.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ export type StreamingProvider = ProviderDescriptor<'streaming'> & {
1111
album?: string,
1212
) => Promise<StreamCandidate[]>;
1313

14+
searchForTrackV2?: (track: Track) => Promise<StreamCandidate[]>;
15+
1416
getStreamUrl: (candidateId: string) => Promise<Stream>;
1517

18+
getStreamUrlV2?: (candidate: StreamCandidate) => Promise<Stream>;
19+
1620
supportsLocalFiles?: boolean;
1721
};
1822

0 commit comments

Comments
 (0)