From 10d02087d05f615222973dffb37cc2422234be76 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Thu, 8 Jan 2026 01:18:12 -0800 Subject: [PATCH] add selector to convert musicbrainz releases to Album type --- .../musicbrainz/api/musicbrainz-api.ts | 141 +++++++++++++++++- src/shared/types/domain-types.ts | 1 + 2 files changed, 138 insertions(+), 4 deletions(-) diff --git a/src/renderer/features/musicbrainz/api/musicbrainz-api.ts b/src/renderer/features/musicbrainz/api/musicbrainz-api.ts index d8c2238b1..5087b3f2b 100644 --- a/src/renderer/features/musicbrainz/api/musicbrainz-api.ts +++ b/src/renderer/features/musicbrainz/api/musicbrainz-api.ts @@ -1,9 +1,17 @@ import { queryOptions } from '@tanstack/react-query'; -import { MusicBrainzApi } from 'musicbrainz-api'; +import memoize from 'lodash/memoize'; +import { IArtist, IRelease, IReleaseGroup, MusicBrainzApi } from 'musicbrainz-api'; import packageJson from '../../../../../package.json'; import { queryKeys } from '/@/renderer/api/query-keys'; +import { + Album, + AlbumArtist, + LibraryItem, + RelatedArtist, + ServerType, +} from '/@/shared/types/domain-types'; export const musicbrainzApi = new MusicBrainzApi({ appContactInfo: packageJson.homepage, @@ -14,20 +22,145 @@ export const musicbrainzApi = new MusicBrainzApi({ // Cache all musicbrainz api results for 5 minutes const CACHE_TIME = 1000 * 60 * 5; +const artistSelect = memoize( + ({ data, meta }: { data: IArtist; meta: { albumArtist: AlbumArtist } }) => { + const releaseGroups = + data['release-groups']?.reduce( + ( + acc: Record< + string, + { + originalDate: null | string; + primaryReleaseType: null | string; + secondaryReleaseTypes: string[]; + } + >, + releaseGroup: IReleaseGroup, + ) => { + const primaryReleaseType = releaseGroup['primary-type'].toLowerCase(); + const secondaryReleaseTypes = releaseGroup['secondary-types'].map((type) => + type.toLowerCase(), + ); + const originalDate = releaseGroup['first-release-date']; + + acc[releaseGroup.title] = { + originalDate: originalDate, + primaryReleaseType: primaryReleaseType, + secondaryReleaseTypes: secondaryReleaseTypes, + }; + + return acc; + }, + {} as Record< + string, + { + originalDate: null | string; + primaryReleaseType: null | string; + secondaryReleaseTypes: string[]; + } + >, + ) || {}; + + const albumArtist: RelatedArtist = { + id: meta.albumArtist.id, + imageId: meta.albumArtist.imageId, + imageUrl: meta.albumArtist.imageUrl, + name: meta.albumArtist.name, + userFavorite: meta.albumArtist.userFavorite, + userRating: meta.albumArtist.userRating, + }; + + const albumArtistName = meta.albumArtist.name; + + const albums: Album[] = (data['releases'] || []) + .map((release: IRelease) => { + const releaseGroup = releaseGroups[release.title]; + + if (!releaseGroup) { + return null; + } + + const releaseType = releaseGroup.primaryReleaseType; + const secondaryReleaseTypes = releaseGroup.secondaryReleaseTypes || []; + const releaseTypes = [releaseType, ...secondaryReleaseTypes].filter( + (type) => type !== null, + ) as string[]; + const isCompilation = releaseTypes.includes('compilation'); + const originalDate = releaseGroup.originalDate; + const originalYear = originalDate ? Number(originalDate.split('-')[0]) : null; + const releaseDate = release.date ? release.date : null; + const releaseYear = release.date ? Number(release.date.split('-')[0]) : null; + const imageUrl = release.media.length > 0 ? getImageUrl(release.id) : null; + + const album: Album = { + _itemType: LibraryItem.ALBUM, + _serverId: '', + _serverType: ServerType.EXTERNAL, + albumArtistName: albumArtistName, + albumArtists: [albumArtist], + artists: [], + comment: null, + createdAt: '', + duration: null, + explicitStatus: null, + genres: [], + id: `musicbrainz-${release.id}`, + imageId: null, + imageUrl: imageUrl, + isCompilation: isCompilation, + lastPlayedAt: null, + mbzId: release.id, + name: release.title, + originalDate: originalDate, + originalYear: originalYear, + participants: {}, + playCount: null, + recordLabels: [], + releaseDate: releaseDate, + releaseType: releaseType, + releaseTypes: releaseTypes, + releaseYear: releaseYear, + size: null, + songCount: null, + tags: {}, + updatedAt: '', + userFavorite: false, + userRating: null, + version: null, + }; + + return album; + }) + .filter((album): album is Album => album !== null); + + return albums; + }, +); + export const musicbrainzQueries = { artist: (args: { mbzArtistId: string }) => { return queryOptions({ gcTime: CACHE_TIME, - queryFn: () => - musicbrainzApi.lookup('artist', args.mbzArtistId, [ + queryFn: async ({ meta }) => { + const data = await musicbrainzApi.lookup('artist', args.mbzArtistId, [ 'releases', + 'release-rels', 'recordings', 'release-groups', + 'release-group-rels', 'works', 'media', - ]), + ]); + + return { data, meta: meta as { albumArtist: AlbumArtist } }; + }, queryKey: queryKeys.musicbrainz.artist(args.mbzArtistId), + select: artistSelect, staleTime: CACHE_TIME, }); }, }; + +function getImageUrl(releaseId: string): string { + return `https://coverartarchive.org/release/${releaseId}/front-250.jpg`; +} diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts index 2890b1ba0..a0e943184 100644 --- a/src/shared/types/domain-types.ts +++ b/src/shared/types/domain-types.ts @@ -34,6 +34,7 @@ export enum LibraryItem { } export enum ServerType { + EXTERNAL = 'external', // This is not an actual server type. This is used when fetching from external sources (e.g. musicbrainz) JELLYFIN = 'jellyfin', NAVIDROME = 'navidrome', SUBSONIC = 'subsonic',