From 0f08a807935dde96f4ec66dcc5917aff33703a4e Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Wed, 3 Jul 2024 08:37:53 +0200 Subject: [PATCH] feat(harmonizer): sort release group types primary type first This also changes HarmonyRelease.types to be an array instead of a set. --- harmonizer/merge.ts | 7 +++--- harmonizer/release_types.test.ts | 26 ++++++++++++++------- harmonizer/release_types.ts | 40 ++++++++++++++++++++++++++++---- harmonizer/types.ts | 2 +- musicbrainz/seeding.ts | 2 +- providers/Bandcamp/mod.ts | 2 +- providers/iTunes/mod.ts | 6 ++--- server/components/Release.tsx | 4 ++-- 8 files changed, 64 insertions(+), 25 deletions(-) diff --git a/harmonizer/merge.ts b/harmonizer/merge.ts index 95a3628..f05b438 100644 --- a/harmonizer/merge.ts +++ b/harmonizer/merge.ts @@ -1,4 +1,5 @@ import { immutableReleaseProperties, immutableTrackProperties } from './properties.ts'; +import { sortTypes } from './release_types.ts'; import { cloneInto, copyTo, filterErrorEntries, isFilled, uniqueMappedValues } from '@/utils/record.ts'; import { similarNames } from '@/utils/similarity.ts'; import { trackCountSummary } from '@/utils/tracklist.ts'; @@ -17,7 +18,6 @@ import type { ProviderPreferences, ProviderReleaseErrorMap, ProviderReleaseMap, - ReleaseGroupType, ResolvableEntity, } from './types.ts'; @@ -59,7 +59,7 @@ export function mergeRelease( artists: [], externalLinks: [], media: [], - types: new Set(), + types: [], info: { providers: [], messages: errorMessages, @@ -127,7 +127,8 @@ export function mergeRelease( // Merge types if (sourceRelease.types) { - mergedRelease.types = mergedRelease.types?.union(sourceRelease.types); + // FIXME: Provide better merge algorithm + mergedRelease.types = sortTypes(new Set(mergedRelease.types).union(new Set(sourceRelease.types))); } // combine availabilities diff --git a/harmonizer/release_types.test.ts b/harmonizer/release_types.test.ts index 600d586..5986346 100644 --- a/harmonizer/release_types.test.ts +++ b/harmonizer/release_types.test.ts @@ -1,4 +1,4 @@ -import { guessLiveRelease, guessTypesForRelease, guessTypesFromTitle } from './release_types.ts'; +import { guessLiveRelease, guessTypesForRelease, guessTypesFromTitle, sortTypes } from './release_types.ts'; import { HarmonyRelease, HarmonyTrack, ReleaseGroupType } from './types.ts'; import { assertEquals } from 'std/assert/assert_equals.ts'; @@ -8,14 +8,14 @@ import type { FunctionSpec } from '../utils/test_spec.ts'; describe('release types', () => { describe('guess types for release', () => { - const passingCases: Array<[string, HarmonyRelease, Set]> = [ - ['should detect EP type from title', makeRelease('Wake of a Nation (EP)'), new Set(['EP'])], - ['should keep existing types', makeRelease('Wake of a Nation (EP)', ['Interview']), new Set(['EP', 'Interview'])], - ['should detect live type from title', makeRelease('One Second (Live)'), new Set(['Live'])], + const passingCases: Array<[string, HarmonyRelease, string[]]> = [ + ['should detect EP type from title', makeRelease('Wake of a Nation (EP)'), ['EP']], + ['should keep existing types', makeRelease('Wake of a Nation (EP)', ['Interview']), ['EP', 'Interview']], + ['should detect live type from title', makeRelease('One Second (Live)'), ['Live']], [ 'should detect live type from tracks', - makeRelease('One Second', null, [{ title: 'One Second - Live' }, { title: 'Darker Thoughts - Live' }]), - new Set(['Live']), + makeRelease('One Second', undefined, [{ title: 'One Second - Live' }, { title: 'Darker Thoughts - Live' }]), + ['Live'], ], ]; @@ -79,11 +79,19 @@ describe('release types', () => { }); }); }); + + describe('sort types', () => { + it('should sort primary type first', () => { + const types: ReleaseGroupType[] = ['Remix', 'Live', 'EP', 'Compilation']; + const sortedTypes = sortTypes(types); + assertEquals(sortedTypes, ['EP', 'Compilation', 'Live', 'Remix']); + }); + }); }); function makeRelease( title: string, - types: ReleaseGroupType[] | null = null, + types: ReleaseGroupType[] | undefined = undefined, tracks: HarmonyTrack[] = [], ): HarmonyRelease { return { @@ -93,7 +101,7 @@ function makeRelease( media: [{ tracklist: tracks, }], - types: types ? new Set(types) : undefined, + types: types, info: { providers: [], messages: [], diff --git a/harmonizer/release_types.ts b/harmonizer/release_types.ts index d5e7049..6dced7d 100644 --- a/harmonizer/release_types.ts +++ b/harmonizer/release_types.ts @@ -1,13 +1,17 @@ import { HarmonyRelease, HarmonyTrack, ReleaseGroupType } from './types.ts'; +import { primaryTypeIds } from '@kellnerd/musicbrainz/data/release-group'; /** Guess the types for a release from release and track titles. */ export function guessTypesForRelease(release: HarmonyRelease) { - let types = release.types || new Set(); + let types = new Set(); + if (release.types) { + types = types.union(new Set(release.types)); + } types = types.union(guessTypesFromTitle(release.title)); if (!types.has('Live') && guessLiveRelease(release.media.flatMap((media) => media.tracklist))) { types.add('Live'); } - release.types = types; + release.types = sortTypes(types); } const detectTypesPatterns = [ @@ -51,11 +55,37 @@ export function guessLiveRelease(tracks: HarmonyTrack[]): boolean { export function convertReleaseType( sourceType: string, typeMap: Record, -): Set { - const types = new Set(); +): ReleaseGroupType[] { + const types: ReleaseGroupType[] = []; const type = typeMap[sourceType]; if (type) { - types.add(type); + types.push(type); } return types; } + +/** Returns a new array with the types sorted, primary types first and secondary types second. */ +export function sortTypes(types: Iterable): ReleaseGroupType[] { + return Array.from(types).sort((a, b) => { + if (a == b) { + return 0; + } + + const primaryA = isPrimaryType(a); + const primaryB = isPrimaryType(b); + + if (primaryA && !primaryB) { + return -1; + } else if (!primaryA && primaryB) { + return 1; + } else { + return a > b ? 1 : -1; + } + }); +} + +const primaryTypes = Object.keys(primaryTypeIds); + +function isPrimaryType(type: ReleaseGroupType): boolean { + return primaryTypes.includes(type); +} diff --git a/harmonizer/types.ts b/harmonizer/types.ts index a3893d3..7d0f1e0 100644 --- a/harmonizer/types.ts +++ b/harmonizer/types.ts @@ -48,7 +48,7 @@ export type HarmonyRelease = { language?: Language; script?: ScriptFrequency; status?: ReleaseStatus; - types?: Set; + types?: ReleaseGroupType[]; releaseDate?: PartialDate; labels?: Label[]; packaging?: ReleasePackaging; diff --git a/musicbrainz/seeding.ts b/musicbrainz/seeding.ts index be200be..e4bfb91 100644 --- a/musicbrainz/seeding.ts +++ b/musicbrainz/seeding.ts @@ -45,7 +45,7 @@ export function createReleaseSeed(release: HarmonyRelease, options: ReleaseSeedO mbid: label.mbid, })), status: release.status, - type: Array.from(release.types?.values() || []), + type: release.types, packaging: release.packaging, mediums: release.media.map((medium) => ({ format: medium.format, diff --git a/providers/Bandcamp/mod.ts b/providers/Bandcamp/mod.ts index 17a45eb..5426d85 100644 --- a/providers/Bandcamp/mod.ts +++ b/providers/Bandcamp/mod.ts @@ -292,7 +292,7 @@ export class BandcampReleaseLookup extends ReleaseLookup } { + private getTypesFromTitle(title: string): { title: string; types: ReleaseGroupType[] } { const re = /\s- (EP|Single)$/; const match = title.match(re); - const types = new Set(); + const types: ReleaseGroupType[] = []; if (match) { title = title.replace(re, ''); - types.add(match[1] as ReleaseGroupType); + types.push(match[1] as ReleaseGroupType); } return { title, types }; diff --git a/server/components/Release.tsx b/server/components/Release.tsx index 1b2f5b9..4e65232 100644 --- a/server/components/Release.tsx +++ b/server/components/Release.tsx @@ -151,10 +151,10 @@ export function Release({ release, releaseMap }: { release: HarmonyRelease; rele {formatScriptFrequency(script)} )} - {types && types.size > 0 && ( + {types && types.length > 0 && ( Types - {Array.from(types).join(' + ')} + {types.join(' + ')} )} {info.providers.length > 1 && (