Skip to content
This repository was archived by the owner on Feb 28, 2026. It is now read-only.

Commit 0b6f765

Browse files
authored
Merge pull request #34 from NuclearPlayer/feat/new-artist-model
New artist model
2 parents 4f06ada + ff15a35 commit 0b6f765

29 files changed

+2721
-736
lines changed

packages/i18n/src/locales/en_US.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,16 @@
113113
"topTracks": "Top tracks",
114114
"onTour": "On Tour",
115115
"notOnTour": "Not on Tour",
116+
"followers": "Followers",
117+
"followings": "Following",
118+
"playlists": "Playlists",
116119
"errors": {
117120
"failedToLoadDetails": "Failed to load artist details.",
118121
"failedToLoadAlbums": "Failed to load albums.",
119122
"failedToLoadPopularTracks": "Failed to load popular tracks.",
120-
"failedToLoadSimilarArtists": "Failed to load similar artists."
123+
"failedToLoadSimilarArtists": "Failed to load similar artists.",
124+
"failedToLoadSocialStats": "Failed to load artist stats.",
125+
"failedToLoadPlaylists": "Failed to load playlists."
121126
}
122127
},
123128
"album": {

packages/model/src/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export type Album = {
8181
source: ProviderRef;
8282
};
8383

84-
export type Artist = {
84+
export type ArtistBio = {
8585
name: string;
8686
disambiguation?: string;
8787
bio?: string;
@@ -91,6 +91,18 @@ export type Artist = {
9191
source: ProviderRef;
9292
};
9393

94+
export type ArtistSocialStats = {
95+
name: string;
96+
artwork?: ArtworkSet;
97+
city?: string;
98+
country?: string;
99+
followersCount?: number;
100+
followingsCount?: number;
101+
trackCount?: number;
102+
playlistCount?: number;
103+
source: ProviderRef;
104+
};
105+
94106
export { pickArtwork } from './artwork';
95107
export type { Playlist, PlaylistIndexEntry, PlaylistItem } from './playlists';
96108
export type { QueueItem, RepeatMode, Queue } from './queue';

packages/player/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,4 @@
7878
"vite-plugin-svgr": "^4.3.0",
7979
"vitest": "^3.2.4"
8080
}
81-
}
81+
}

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,14 @@ describe('metadataHost', () => {
153153
describe('artist metadata', () => {
154154
it.each([
155155
{
156-
method: 'fetchArtistDetails',
157-
mockMethod: 'withFetchArtistDetails',
158-
capability: 'artistDetails',
156+
method: 'fetchArtistBio',
157+
mockMethod: 'withFetchArtistBio',
158+
capability: 'artistBio',
159+
},
160+
{
161+
method: 'fetchArtistSocialStats',
162+
mockMethod: 'withFetchArtistSocialStats',
163+
capability: 'artistSocialStats',
159164
},
160165
{
161166
method: 'fetchArtistAlbums',
@@ -167,6 +172,11 @@ describe('metadataHost', () => {
167172
mockMethod: 'withFetchArtistTopTracks',
168173
capability: 'artistTopTracks',
169174
},
175+
{
176+
method: 'fetchArtistPlaylists',
177+
mockMethod: 'withFetchArtistPlaylists',
178+
capability: 'artistPlaylists',
179+
},
170180
{
171181
method: 'fetchArtistRelatedArtists',
172182
mockMethod: 'withFetchArtistRelatedArtists',
@@ -199,9 +209,11 @@ describe('metadataHost', () => {
199209
});
200210

201211
it.each([
202-
{ method: 'fetchArtistDetails', capability: 'artistDetails' },
212+
{ method: 'fetchArtistBio', capability: 'artistBio' },
213+
{ method: 'fetchArtistSocialStats', capability: 'artistSocialStats' },
203214
{ method: 'fetchArtistAlbums', capability: 'artistAlbums' },
204215
{ method: 'fetchArtistTopTracks', capability: 'artistTopTracks' },
216+
{ method: 'fetchArtistPlaylists', capability: 'artistPlaylists' },
205217
{
206218
method: 'fetchArtistRelatedArtists',
207219
capability: 'artistRelatedArtists',

packages/player/src/services/metadataHost.ts

Lines changed: 43 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import type {
22
Album,
33
AlbumRef,
4-
Artist,
4+
ArtistBio,
55
ArtistRef,
6+
ArtistSocialStats,
7+
PlaylistRef,
68
SearchCategory,
79
SearchParams,
810
SearchResults,
911
TrackRef,
1012
} from '@nuclearplayer/model';
1113
import {
1214
MissingCapabilityError,
15+
type ArtistMetadataCapability,
1316
type MetadataHost,
1417
type MetadataProvider,
1518
} from '@nuclearplayer/plugin-sdk';
@@ -104,75 +107,58 @@ export const createMetadataHost = (): MetadataHost => {
104107
return providers[0] as MetadataProvider | undefined;
105108
};
106109

107-
return {
108-
search: async (
109-
params: SearchParams,
110-
providerId?: string,
111-
): Promise<SearchResults> => {
112-
const provider = getProvider(providerId);
113-
if (!provider) {
114-
throw new Error('No metadata provider available');
115-
}
116-
return executeMetadataSearch(provider, params);
117-
},
118-
119-
fetchArtistDetails: async (
120-
artistId: string,
121-
providerId?: string,
122-
): Promise<Artist> => {
110+
const withArtistCapability =
111+
<TResult>(
112+
capability: ArtistMetadataCapability,
113+
method: keyof MetadataProvider,
114+
) =>
115+
async (entityId: string, providerId?: string): Promise<TResult> => {
123116
const provider = getProvider(providerId);
124117
if (!provider) {
125118
throw new Error('No metadata provider available');
126119
}
127-
if (!provider.artistMetadataCapabilities?.includes('artistDetails')) {
128-
throw new MissingCapabilityError('artistDetails');
120+
if (!provider.artistMetadataCapabilities?.includes(capability)) {
121+
throw new MissingCapabilityError(capability);
129122
}
130-
return provider.fetchArtistDetails!(artistId)!;
131-
},
123+
return (provider[method] as (id: string) => Promise<TResult>)!(entityId);
124+
};
132125

133-
fetchArtistAlbums: async (
134-
artistId: string,
135-
providerId?: string,
136-
): Promise<AlbumRef[]> => {
137-
const provider = getProvider(providerId);
138-
if (!provider) {
139-
throw new Error('No metadata provider available');
140-
}
141-
if (!provider.artistMetadataCapabilities?.includes('artistAlbums')) {
142-
throw new MissingCapabilityError('artistAlbums');
143-
}
144-
return provider.fetchArtistAlbums!(artistId)!;
145-
},
146-
147-
fetchArtistTopTracks: async (
148-
artistId: string,
126+
return {
127+
search: async (
128+
params: SearchParams,
149129
providerId?: string,
150-
): Promise<TrackRef[]> => {
130+
): Promise<SearchResults> => {
151131
const provider = getProvider(providerId);
152132
if (!provider) {
153133
throw new Error('No metadata provider available');
154134
}
155-
if (!provider.artistMetadataCapabilities?.includes('artistTopTracks')) {
156-
throw new MissingCapabilityError('artistTopTracks');
157-
}
158-
return provider.fetchArtistTopTracks!(artistId)!;
135+
return executeMetadataSearch(provider, params);
159136
},
160137

161-
fetchArtistRelatedArtists: async (
162-
artistId: string,
163-
providerId?: string,
164-
): Promise<ArtistRef[]> => {
165-
const provider = getProvider(providerId);
166-
if (!provider) {
167-
throw new Error('No metadata provider available');
168-
}
169-
if (
170-
!provider.artistMetadataCapabilities?.includes('artistRelatedArtists')
171-
) {
172-
throw new MissingCapabilityError('artistRelatedArtists');
173-
}
174-
return provider.fetchArtistRelatedArtists!(artistId)!;
175-
},
138+
fetchArtistBio: withArtistCapability<ArtistBio>(
139+
'artistBio',
140+
'fetchArtistBio',
141+
),
142+
fetchArtistSocialStats: withArtistCapability<ArtistSocialStats>(
143+
'artistSocialStats',
144+
'fetchArtistSocialStats',
145+
),
146+
fetchArtistAlbums: withArtistCapability<AlbumRef[]>(
147+
'artistAlbums',
148+
'fetchArtistAlbums',
149+
),
150+
fetchArtistTopTracks: withArtistCapability<TrackRef[]>(
151+
'artistTopTracks',
152+
'fetchArtistTopTracks',
153+
),
154+
fetchArtistPlaylists: withArtistCapability<PlaylistRef[]>(
155+
'artistPlaylists',
156+
'fetchArtistPlaylists',
157+
),
158+
fetchArtistRelatedArtists: withArtistCapability<ArtistRef[]>(
159+
'artistRelatedArtists',
160+
'fetchArtistRelatedArtists',
161+
),
176162

177163
fetchAlbumDetails: async (
178164
albumId: string,

packages/player/src/test/builders/MetadataProviderBuilder.ts

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,53 @@
11
import { MetadataProvider } from '@nuclearplayer/plugin-sdk';
22

3+
import {
4+
ALBUMS_BEATLES,
5+
BIO_BEATLES,
6+
PLAYLISTS_DEADMAU5,
7+
RELATED_ARTISTS_BEATLES,
8+
RELATED_ARTISTS_DEADMAU5,
9+
SEARCH_RESULT,
10+
SOCIAL_STATS_DEADMAU5,
11+
TOP_TRACKS_BEATLES,
12+
TOP_TRACKS_DEADMAU5,
13+
} from '../fixtures/artists';
14+
315
export class MetadataProviderBuilder {
416
private provider: MetadataProvider;
517

18+
static bioStyleProvider(): MetadataProviderBuilder {
19+
return new MetadataProviderBuilder()
20+
.withSearchCapabilities(['unified', 'artists'])
21+
.withArtistMetadataCapabilities([
22+
'artistBio',
23+
'artistAlbums',
24+
'artistTopTracks',
25+
'artistRelatedArtists',
26+
])
27+
.withAlbumMetadataCapabilities(['albumDetails'])
28+
.withSearch(async () => SEARCH_RESULT)
29+
.withFetchArtistBio(async () => BIO_BEATLES)
30+
.withFetchArtistAlbums(async () => ALBUMS_BEATLES)
31+
.withFetchArtistTopTracks(async () => TOP_TRACKS_BEATLES)
32+
.withFetchArtistRelatedArtists(async () => RELATED_ARTISTS_BEATLES);
33+
}
34+
35+
static socialStatsStyleProvider(): MetadataProviderBuilder {
36+
return new MetadataProviderBuilder()
37+
.withSearchCapabilities(['unified', 'artists'])
38+
.withArtistMetadataCapabilities([
39+
'artistSocialStats',
40+
'artistTopTracks',
41+
'artistPlaylists',
42+
'artistRelatedArtists',
43+
])
44+
.withSearch(async () => SEARCH_RESULT)
45+
.withFetchArtistSocialStats(async () => SOCIAL_STATS_DEADMAU5)
46+
.withFetchArtistTopTracks(async () => TOP_TRACKS_DEADMAU5)
47+
.withFetchArtistPlaylists(async () => PLAYLISTS_DEADMAU5)
48+
.withFetchArtistRelatedArtists(async () => RELATED_ARTISTS_DEADMAU5);
49+
}
50+
651
constructor() {
752
this.provider = {
853
id: 'test-metadata-provider',
@@ -74,10 +119,15 @@ export class MetadataProviderBuilder {
74119
return this;
75120
}
76121

77-
withFetchArtistDetails(
78-
fetchArtistDetails: MetadataProvider['fetchArtistDetails'],
122+
withFetchArtistBio(fetchArtistBio: MetadataProvider['fetchArtistBio']): this {
123+
this.provider.fetchArtistBio = fetchArtistBio;
124+
return this;
125+
}
126+
127+
withFetchArtistSocialStats(
128+
fetchArtistSocialStats: MetadataProvider['fetchArtistSocialStats'],
79129
): this {
80-
this.provider.fetchArtistDetails = fetchArtistDetails;
130+
this.provider.fetchArtistSocialStats = fetchArtistSocialStats;
81131
return this;
82132
}
83133

@@ -102,6 +152,13 @@ export class MetadataProviderBuilder {
102152
return this;
103153
}
104154

155+
withFetchArtistPlaylists(
156+
fetchArtistPlaylists: MetadataProvider['fetchArtistPlaylists'],
157+
): this {
158+
this.provider.fetchArtistPlaylists = fetchArtistPlaylists;
159+
return this;
160+
}
161+
105162
withFetchAlbumDetails(
106163
fetchAlbumDetails: MetadataProvider['fetchAlbumDetails'],
107164
): this {

0 commit comments

Comments
 (0)