From f6cec17710d3d7328f81fc3fc25451fb4e44745e Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 18 Jan 2026 16:16:41 -0800 Subject: [PATCH] progress --- .../api/jellyfin/jellyfin-controller.ts | 6 +- src/renderer/api/query-keys.ts | 21 +- .../album-artist-detail-content.tsx | 15 +- .../musicbrainz/api/musicbrainz-api.ts | 569 ++++++++++++++++-- src/renderer/store/settings.store.ts | 4 + 5 files changed, 547 insertions(+), 68 deletions(-) diff --git a/src/renderer/api/jellyfin/jellyfin-controller.ts b/src/renderer/api/jellyfin/jellyfin-controller.ts index 20dc2570e..8bbfc8025 100644 --- a/src/renderer/api/jellyfin/jellyfin-controller.ts +++ b/src/renderer/api/jellyfin/jellyfin-controller.ts @@ -242,7 +242,7 @@ export const JellyfinController: InternalControllerEndpoint = { userId: apiClientProps.server?.userId, }, query: { - Fields: JF_FIELDS.ALBUM_ARTIST_DETAIL, + Fields: 'Genres, Overview, SortName, ProviderIds', }, }), jfApiClient(apiClientProps).getSimilarArtistList({ @@ -269,7 +269,7 @@ export const JellyfinController: InternalControllerEndpoint = { const res = await jfApiClient(apiClientProps).getAlbumArtistList({ query: { - Fields: JF_FIELDS.ALBUM_ARTIST_LIST, + Fields: 'Genres, DateCreated, ExternalUrls, Overview, SortName, ProviderIds', ImageTypeLimit: 1, Limit: query.limit, ParentId: getLibraryId(query.musicFolderId), @@ -321,7 +321,7 @@ export const JellyfinController: InternalControllerEndpoint = { userId: apiClientProps.server.userId, }, query: { - Fields: JF_FIELDS.SONG, + Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags, SortName, ProviderIds', IncludeItemTypes: 'Audio', ParentId: query.id, SortBy: 'ParentIndexNumber,IndexNumber,SortName', diff --git a/src/renderer/api/query-keys.ts b/src/renderer/api/query-keys.ts index 87120df64..5b03e9f2b 100644 --- a/src/renderer/api/query-keys.ts +++ b/src/renderer/api/query-keys.ts @@ -271,7 +271,26 @@ export const queryKeys: Record< root: (serverId: string) => [serverId, 'genres'] as const, }, musicbrainz: { - artist: (mbzArtistId: string) => ['musicbrainz', 'artist', mbzArtistId] as const, + artist: ( + limit: number | undefined, + mbzArtistId: string, + config?: { + excludeReleaseTypes: string[]; + prioritizeCountries: string[]; + }, + ) => + [ + 'musicbrainz', + 'artist', + mbzArtistId, + limit, + config + ? [ + [...config.excludeReleaseTypes].sort().join(','), + [...config.prioritizeCountries].sort().join(','), + ] + : null, + ] as const, root: () => ['musicbrainz'] as const, }, musicFolders: { diff --git a/src/renderer/features/artists/components/album-artist-detail-content.tsx b/src/renderer/features/artists/components/album-artist-detail-content.tsx index ba324907e..5d85434d1 100644 --- a/src/renderer/features/artists/components/album-artist-detail-content.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-content.tsx @@ -1358,6 +1358,12 @@ interface ArtistAlbumsProps { const ArtistAlbums = ({ albumsQuery }: ArtistAlbumsProps) => { const { t } = useTranslation(); const artistReleaseTypeItems = useArtistReleaseTypeItems(); + const musicBrainzExcludeReleaseTypes = useSettingsStore( + (state) => state.general.musicBrainzExcludeReleaseTypes, + ); + const musicBrainzPrioritizeCountries = useSettingsStore( + (state) => state.general.musicBrainzPrioritizeCountries, + ); const [searchTerm, setSearchTerm] = useState(''); const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300); const albumArtistDetailSort = useAppStore((state) => state.albumArtistDetailSort); @@ -1382,9 +1388,16 @@ const ArtistAlbums = ({ albumsQuery }: ArtistAlbumsProps) => { ); const musicbrainzArtistQuery = useQuery({ - ...musicbrainzQueries.artist({ mbzArtistId: detailQuery.data?.mbz as string }), + ...musicbrainzQueries.artist({ + excludeReleaseTypes: musicBrainzExcludeReleaseTypes, + mbzArtistId: detailQuery.data?.mbz as string, + prioritizeCountries: musicBrainzPrioritizeCountries, + }), meta: { albumArtist: detailQuery.data, + albums: albumsQuery.data?.items || [], + excludeReleaseTypes: musicBrainzExcludeReleaseTypes, + prioritizeCountries: musicBrainzPrioritizeCountries, }, }); diff --git a/src/renderer/features/musicbrainz/api/musicbrainz-api.ts b/src/renderer/features/musicbrainz/api/musicbrainz-api.ts index 5087b3f2b..213eee71b 100644 --- a/src/renderer/features/musicbrainz/api/musicbrainz-api.ts +++ b/src/renderer/features/musicbrainz/api/musicbrainz-api.ts @@ -1,6 +1,12 @@ import { queryOptions } from '@tanstack/react-query'; import memoize from 'lodash/memoize'; -import { IArtist, IRelease, IReleaseGroup, MusicBrainzApi } from 'musicbrainz-api'; +import { + IArtist, + IBrowseReleasesResult, + IRelease, + IReleaseGroup, + MusicBrainzApi, +} from 'musicbrainz-api'; import packageJson from '../../../../../package.json'; @@ -19,48 +25,28 @@ export const musicbrainzApi = new MusicBrainzApi({ appVersion: packageJson.version, }); -// Cache all musicbrainz api results for 5 minutes const CACHE_TIME = 1000 * 60 * 5; +export type MusicBrainzArtistSelectMeta = { + albumArtist: AlbumArtist; + albums?: Album[]; + /** Release types to exclude (e.g. 'single', 'ep'). Matches primary and secondary types. */ + excludeReleaseTypes?: string[]; + /** Country codes (e.g. 'US', 'GB') to sort releases by; earlier in the list = higher priority. */ + prioritizeCountries?: string[]; +}; + 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[]; - } - >, - ) || {}; - + ({ + data, + meta, + }: { + data: { + artist: IArtist; + releases: IBrowseReleasesResult; + }; + meta: MusicBrainzArtistSelectMeta; + }) => { const albumArtist: RelatedArtist = { id: meta.albumArtist.id, imageId: meta.albumArtist.imageId, @@ -70,27 +56,138 @@ const artistSelect = memoize( userRating: meta.albumArtist.userRating, }; + console.log('meta', meta); + + const ownedMbzReleaseGroupIds = new Set(); + const ownedMbzReleaseIds = new Set(); + + const counts = { + existingMbzReleaseGroupIds: 0, + existingMbzReleaseIds: 0, + }; + + for (const album of meta.albums || []) { + if (album.mbzReleaseGroupId) { + ownedMbzReleaseGroupIds.add(album.mbzReleaseGroupId); + counts.existingMbzReleaseGroupIds++; + } + + if (album.mbzId) { + ownedMbzReleaseIds.add(album.mbzId); + counts.existingMbzReleaseIds++; + } + } + + console.log('existingMbzReleaseGroupIds', ownedMbzReleaseGroupIds); + console.log('existingMbzReleaseIds', ownedMbzReleaseIds); + console.log('counts', counts); + const albumArtistName = meta.albumArtist.name; - const albums: Album[] = (data['releases'] || []) - .map((release: IRelease) => { - const releaseGroup = releaseGroups[release.title]; + // const releaseGroupMap = new Map< + // string, + // { + // release: IRelease; + // releaseGroup: NonNullable; + // score: number; + // } + // >(); - if (!releaseGroup) { - return null; + const existingReleaseGroups = new Map(); + const existingReleases = new Map(); + const unownedReleases = new Map(); + const unownedReleaseGroups = new Map(); + + for (const release of data.releases.releases) { + const releaseGroup = release['release-group']; + const hasReleaseGroup = releaseGroup?.id !== undefined; + + if (hasReleaseGroup && ownedMbzReleaseGroupIds.has(releaseGroup.id)) { + existingReleaseGroups.set(releaseGroup.id, release); + } + + if (ownedMbzReleaseIds.has(release.id)) { + existingReleases.set(release.id, release); + } + } + + console.log('existingReleaseGroups', existingReleaseGroups); + console.log('existingReleases', existingReleases); + + for (const release of data.releases.releases) { + const releaseGroupId = release['release-group']?.id; + if ( + releaseGroupId && + !ownedMbzReleaseIds.has(release.id) && + !ownedMbzReleaseGroupIds.has(releaseGroupId) + ) { + unownedReleases.set(release.id, release); + if (releaseGroupId && release['release-group']) { + unownedReleaseGroups.set(releaseGroupId, release['release-group']); } + } else if (!releaseGroupId && !ownedMbzReleaseIds.has(release.id)) { + console.log('adding unowned release by release id', release.id); + unownedReleases.set(release.id, release); + } + } - const releaseType = releaseGroup.primaryReleaseType; - const secondaryReleaseTypes = releaseGroup.secondaryReleaseTypes || []; - const releaseTypes = [releaseType, ...secondaryReleaseTypes].filter( + console.log('unownedReleases', unownedReleases); + console.log('unownedReleaseGroups', unownedReleaseGroups); + const excludeReleaseTypes = (meta.excludeReleaseTypes ?? []).map((t) => t.toLowerCase()); + const excludeSet = new Set(excludeReleaseTypes); + const prioritizeCountries = (meta.prioritizeCountries ?? []).map((c) => c.toUpperCase()); + + const releaseEntries = Array.from(unownedReleases.entries()) + .filter(([, release]) => { + if (excludeSet.size === 0) return true; + const releaseGroup = release['release-group']; + const primary = releaseGroup?.['primary-type']?.toLowerCase(); + const secondary = + releaseGroup?.['secondary-types']?.map((t) => t.toLowerCase()) ?? []; + const types = [primary, ...secondary].filter(Boolean) as string[]; + return !types.some((t) => excludeSet.has(t)); + }) + .sort(([, a], [, b]) => { + if (prioritizeCountries.length === 0) return 0; + const indexA = a.country + ? prioritizeCountries.indexOf(a.country.toUpperCase()) + : -1; + const indexB = b.country + ? prioritizeCountries.indexOf(b.country.toUpperCase()) + : -1; + const posA = indexA === -1 ? Number.MAX_SAFE_INTEGER : indexA; + const posB = indexB === -1 ? Number.MAX_SAFE_INTEGER : indexB; + return posA - posB; + }); + + const seenReleaseGroupIds = new Set(); + const releaseEntriesUniqueByGroup = releaseEntries.filter(([, release]) => { + const releaseGroupId = release['release-group']?.id; + if (releaseGroupId == null) return true; + if (seenReleaseGroupIds.has(releaseGroupId)) return false; + seenReleaseGroupIds.add(releaseGroupId); + return true; + }); + + const albums: Album[] = releaseEntriesUniqueByGroup + .map(([releaseId, release]) => { + const releaseGroup = release['release-group']; + const hasArtwork = + release['cover-art-archive']?.artwork === true && + release['cover-art-archive']?.front === true; + + const primaryReleaseType = releaseGroup?.['primary-type']?.toLowerCase() || null; + const secondaryReleaseTypes = + releaseGroup?.['secondary-types']?.map((type) => type.toLowerCase()) || []; + const releaseTypes = [primaryReleaseType, ...secondaryReleaseTypes].filter( (type) => type !== null, ) as string[]; const isCompilation = releaseTypes.includes('compilation'); - const originalDate = releaseGroup.originalDate; + const originalDate = releaseGroup?.['first-release-date'] || null; 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 imageUrl = hasArtwork ? getImageUrl(releaseId) : null; const album: Album = { _itemType: LibraryItem.ALBUM, @@ -110,6 +207,7 @@ const artistSelect = memoize( isCompilation: isCompilation, lastPlayedAt: null, mbzId: release.id, + mbzReleaseGroupId: releaseGroup?.id || null, name: release.title, originalDate: originalDate, originalYear: originalYear, @@ -117,11 +215,12 @@ const artistSelect = memoize( playCount: null, recordLabels: [], releaseDate: releaseDate, - releaseType: releaseType, + releaseType: primaryReleaseType, releaseTypes: releaseTypes, releaseYear: releaseYear, size: null, songCount: null, + sortName: release.title, tags: {}, updatedAt: '', userFavorite: false, @@ -137,24 +236,83 @@ const artistSelect = memoize( }, ); +async function fetchAllReleases(mbzArtistId: string): Promise { + const PAGE_SIZE = 100; + const includes: Array<'media' | 'release-groups'> = ['media', 'release-groups']; + + // Fetch first page to get total count + const firstPage = (await musicbrainzApi.browse( + 'release', + { + artist: mbzArtistId, + limit: PAGE_SIZE, + offset: 0, + }, + includes, + )) as unknown as IBrowseReleasesResult; + + const totalCount = firstPage['release-count']; + const allReleases = [...firstPage.releases]; + + // If we got all releases in the first page, return early + if (allReleases.length >= totalCount) { + return firstPage; + } + + // Calculate number of additional pages needed + const remainingCount = totalCount - allReleases.length; + const numberOfPages = Math.ceil(remainingCount / PAGE_SIZE); + + // Fetch all remaining pages in parallel + const pagePromises = Array.from({ length: numberOfPages }, (_, i) => { + const offset = (i + 1) * PAGE_SIZE; + return musicbrainzApi.browse( + 'release', + { + artist: mbzArtistId, + limit: PAGE_SIZE, + offset: offset, + }, + includes, + ) as unknown as Promise; + }); + + const remainingPages = await Promise.all(pagePromises); + + for (const page of remainingPages) { + allReleases.push(...page.releases); + } + + return { + 'release-count': totalCount, + 'release-offset': 0, + releases: allReleases, + }; +} + export const musicbrainzQueries = { - artist: (args: { mbzArtistId: string }) => { + artist: (args: { + excludeReleaseTypes?: string[]; + mbzArtistId: string; + prioritizeCountries?: string[]; + }) => { + const config = { + excludeReleaseTypes: args.excludeReleaseTypes ?? [], + prioritizeCountries: args.prioritizeCountries ?? [], + }; + return queryOptions({ gcTime: CACHE_TIME, queryFn: async ({ meta }) => { - const data = await musicbrainzApi.lookup('artist', args.mbzArtistId, [ - 'releases', - 'release-rels', - 'recordings', - 'release-groups', - 'release-group-rels', - 'works', - 'media', - ]); + const artist = await musicbrainzApi.lookup('artist', args.mbzArtistId); + const releases = await fetchAllReleases(args.mbzArtistId); - return { data, meta: meta as { albumArtist: AlbumArtist } }; + return { + data: { artist, releases }, + meta: meta as MusicBrainzArtistSelectMeta, + }; }, - queryKey: queryKeys.musicbrainz.artist(args.mbzArtistId), + queryKey: queryKeys.musicbrainz.artist(undefined, args.mbzArtistId, config), select: artistSelect, staleTime: CACHE_TIME, }); @@ -164,3 +322,288 @@ export const musicbrainzQueries = { function getImageUrl(releaseId: string): string { return `https://coverartarchive.org/release/${releaseId}/front-250.jpg`; } + +function getImageUrlByReleaseGroupId(releaseGroupId: string): string { + return `https://coverartarchive.org/release-group/${releaseGroupId}/front-250.jpg`; +} + +const MBZ_COUNTRY_CODES = { + AD: 'Andorra', + AE: 'United Arab Emirates', + AF: 'Afghanistan', + AG: 'Antigua and Barbuda', + AI: 'Anguilla', + AL: 'Albania', + AM: 'Armenia', + AN: 'Netherlands Antilles', + AO: 'Angola', + AQ: 'Antarctica', + AR: 'Argentina', + AS: 'American Samoa', + AT: 'Austria', + AU: 'Australia', + AW: 'Aruba', + AX: 'Åland Islands', + AZ: 'Azerbaijan', + BA: 'Bosnia and Herzegovina', + BB: 'Barbados', + BD: 'Bangladesh', + BE: 'Belgium', + BF: 'Burkina Faso', + BG: 'Bulgaria', + BH: 'Bahrain', + BI: 'Burundi', + BJ: 'Benin', + BL: 'Saint Barthélemy', + BM: 'Bermuda', + BN: 'Brunei', + BO: 'Bolivia', + BQ: 'Bonaire, Sint Eustatius and Saba', + BR: 'Brazil', + BS: 'Bahamas', + BT: 'Bhutan', + BV: 'Bouvet Island', + BW: 'Botswana', + BY: 'Belarus', + BZ: 'Belize', + CA: 'Canada', + CC: 'Cocos (Keeling) Islands', + CD: 'Democratic Republic of the Congo', + CF: 'Central African Republic', + CG: 'Congo', + CH: 'Switzerland', + CI: "Côte d'Ivoire", + CK: 'Cook Islands', + CL: 'Chile', + CM: 'Cameroon', + CN: 'China', + CO: 'Colombia', + CR: 'Costa Rica', + CS: 'Serbia and Montenegro', + CU: 'Cuba', + CV: 'Cape Verde', + CW: 'Curaçao', + CX: 'Christmas Island', + CY: 'Cyprus', + CZ: 'Czechia', + DE: 'Germany', + DJ: 'Djibouti', + DK: 'Denmark', + DM: 'Dominica', + DO: 'Dominican Republic', + DZ: 'Algeria', + EC: 'Ecuador', + EE: 'Estonia', + EG: 'Egypt', + EH: 'Western Sahara', + ER: 'Eritrea', + ES: 'Spain', + ET: 'Ethiopia', + FI: 'Finland', + FJ: 'Fiji', + FK: 'Falkland Islands', + FM: 'Federated States of Micronesia', + FO: 'Faroe Islands', + FR: 'France', + GA: 'Gabon', + GB: 'United Kingdom', + GD: 'Grenada', + GE: 'Georgia', + GF: 'French Guiana', + GG: 'Guernsey', + GH: 'Ghana', + GI: 'Gibraltar', + GL: 'Greenland', + GM: 'Gambia', + GN: 'Guinea', + GP: 'Guadeloupe', + GQ: 'Equatorial Guinea', + GR: 'Greece', + GS: 'South Georgia and the South Sandwich Islands', + GT: 'Guatemala', + GU: 'Guam', + GW: 'Guinea-Bissau', + GY: 'Guyana', + HK: 'Hong Kong', + HM: 'Heard Island and McDonald Islands', + HN: 'Honduras', + HR: 'Croatia', + HT: 'Haiti', + HU: 'Hungary', + ID: 'Indonesia', + IE: 'Ireland', + IL: 'Israel', + IM: 'Isle of Man', + IN: 'India', + IO: 'British Indian Ocean Territory', + IQ: 'Iraq', + IR: 'Iran', + IS: 'Iceland', + IT: 'Italy', + JE: 'Jersey', + JM: 'Jamaica', + JO: 'Jordan', + JP: 'Japan', + KE: 'Kenya', + KG: 'Kyrgyzstan', + KH: 'Cambodia', + KI: 'Kiribati', + KM: 'Comoros', + KN: 'Saint Kitts and Nevis', + KP: 'North Korea', + KR: 'South Korea', + KW: 'Kuwait', + KY: 'Cayman Islands', + KZ: 'Kazakhstan', + LA: 'Laos', + LB: 'Lebanon', + LC: 'Saint Lucia', + LI: 'Liechtenstein', + LK: 'Sri Lanka', + LR: 'Liberia', + LS: 'Lesotho', + LT: 'Lithuania', + LU: 'Luxembourg', + LV: 'Latvia', + LY: 'Libya', + MA: 'Morocco', + MC: 'Monaco', + MD: 'Moldova', + ME: 'Montenegro', + MF: 'Saint Martin (French part)', + MG: 'Madagascar', + MH: 'Marshall Islands', + MK: 'North Macedonia', + ML: 'Mali', + MM: 'Myanmar', + MN: 'Mongolia', + MO: 'Macao', + MP: 'Northern Mariana Islands', + MQ: 'Martinique', + MR: 'Mauritania', + MS: 'Montserrat', + MT: 'Malta', + MU: 'Mauritius', + MV: 'Maldives', + MW: 'Malawi', + MX: 'Mexico', + MY: 'Malaysia', + MZ: 'Mozambique', + NA: 'Namibia', + NC: 'New Caledonia', + NE: 'Niger', + NF: 'Norfolk Island', + NG: 'Nigeria', + NI: 'Nicaragua', + NL: 'Netherlands', + NO: 'Norway', + NP: 'Nepal', + NR: 'Nauru', + NU: 'Niue', + NZ: 'New Zealand', + OM: 'Oman', + PA: 'Panama', + PE: 'Peru', + PF: 'French Polynesia', + PG: 'Papua New Guinea', + PH: 'Philippines', + PK: 'Pakistan', + PL: 'Poland', + PM: 'Saint Pierre and Miquelon', + PN: 'Pitcairn', + PR: 'Puerto Rico', + PS: 'Palestine', + PT: 'Portugal', + PW: 'Palau', + PY: 'Paraguay', + QA: 'Qatar', + RE: 'Réunion', + RO: 'Romania', + RS: 'Serbia', + RU: 'Russia', + RW: 'Rwanda', + SA: 'Saudi Arabia', + SB: 'Solomon Islands', + SC: 'Seychelles', + SD: 'Sudan', + SE: 'Sweden', + SG: 'Singapore', + SH: 'Saint Helena, Ascension and Tristan da Cunha', + SI: 'Slovenia', + SJ: 'Svalbard and Jan Mayen', + SK: 'Slovakia', + SL: 'Sierra Leone', + SM: 'San Marino', + SN: 'Senegal', + SO: 'Somalia', + SR: 'Suriname', + SS: 'South Sudan', + ST: 'Sao Tome and Principe', + SU: 'Soviet Union', + SV: 'El Salvador', + SX: 'Sint Maarten (Dutch part)', + SY: 'Syria', + SZ: 'Eswatini', + TC: 'Turks and Caicos Islands', + TD: 'Chad', + TF: 'French Southern Territories', + TG: 'Togo', + TH: 'Thailand', + TJ: 'Tajikistan', + TK: 'Tokelau', + TL: 'Timor-Leste', + TM: 'Turkmenistan', + TN: 'Tunisia', + TO: 'Tonga', + TR: 'Turkey', + TT: 'Trinidad and Tobago', + TV: 'Tuvalu', + TW: 'Taiwan', + TZ: 'Tanzania', + UA: 'Ukraine', + UG: 'Uganda', + UM: 'United States Minor Outlying Islands', + US: 'United States', + UY: 'Uruguay', + UZ: 'Uzbekistan', + VA: 'Vatican City', + VC: 'Saint Vincent and The Grenadines', + VE: 'Venezuela', + VG: 'British Virgin Islands', + VI: 'U.S. Virgin Islands', + VN: 'Vietnam', + VU: 'Vanuatu', + WF: 'Wallis and Futuna', + WS: 'Samoa', + XC: 'Czechoslovakia', + XE: 'Europe', + XG: 'East Germany', + XK: 'Kosovo', + XW: '[Worldwide]', + YE: 'Yemen', + YT: 'Mayotte', + YU: 'Yugoslavia', + ZA: 'South Africa', + ZM: 'Zambia', + ZW: 'Zimbabwe', +}; + +const MBZ_RELEASE_TYPES = { + album: 'album', + audiobook: 'audiobook', + 'audio drama': 'audio drama', + broadcast: 'broadcast', + compilation: 'compilation', + demo: 'demo', + 'dj-mix': 'dj-mix', + ep: 'ep', + 'field recording': 'field recording', + interview: 'interview', + live: 'live', + 'mixtape/street': 'mixtape/street', + other: 'other', + remix: 'remix', + single: 'single', + soundtrack: 'soundtrack', + spokenword: 'spokenword', +}; diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index 6beef0f09..eb59cfee6 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -440,6 +440,8 @@ export const GeneralSettingsSchema = z.object({ lastFM: z.boolean(), lastfmApiKey: z.string(), musicBrainz: z.boolean(), + musicBrainzExcludeReleaseTypes: z.array(z.string()), + musicBrainzPrioritizeCountries: z.array(z.string()), nativeAspectRatio: z.boolean(), passwordStore: z.string().optional(), pathReplace: z.string(), @@ -1013,6 +1015,8 @@ const initialState: SettingsState = { lastFM: true, lastfmApiKey: '', musicBrainz: true, + musicBrainzExcludeReleaseTypes: [], + musicBrainzPrioritizeCountries: [], nativeAspectRatio: false, passwordStore: undefined, pathReplace: '',