Skip to content

Commit

Permalink
feat(harmonizer): sort release group types primary type first
Browse files Browse the repository at this point in the history
This also changes HarmonyRelease.types to be an array instead of a set.
  • Loading branch information
phw committed Jul 3, 2024
1 parent d91fbe4 commit 0f08a80
Show file tree
Hide file tree
Showing 8 changed files with 64 additions and 25 deletions.
7 changes: 4 additions & 3 deletions harmonizer/merge.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -17,7 +18,6 @@ import type {
ProviderPreferences,
ProviderReleaseErrorMap,
ProviderReleaseMap,
ReleaseGroupType,
ResolvableEntity,
} from './types.ts';

Expand Down Expand Up @@ -59,7 +59,7 @@ export function mergeRelease(
artists: [],
externalLinks: [],
media: [],
types: new Set<ReleaseGroupType>(),
types: [],
info: {
providers: [],
messages: errorMessages,
Expand Down Expand Up @@ -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
Expand Down
26 changes: 17 additions & 9 deletions harmonizer/release_types.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<string>]> = [
['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'],
],
];

Expand Down Expand Up @@ -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 {
Expand All @@ -93,7 +101,7 @@ function makeRelease(
media: [{
tracklist: tracks,
}],
types: types ? new Set(types) : undefined,
types: types,
info: {
providers: [],
messages: [],
Expand Down
40 changes: 35 additions & 5 deletions harmonizer/release_types.ts
Original file line number Diff line number Diff line change
@@ -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<ReleaseGroupType>();
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 = [
Expand Down Expand Up @@ -51,11 +55,37 @@ export function guessLiveRelease(tracks: HarmonyTrack[]): boolean {
export function convertReleaseType(
sourceType: string,
typeMap: Record<string, ReleaseGroupType>,
): Set<ReleaseGroupType> {
const types = new Set<ReleaseGroupType>();
): 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>): 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);
}
2 changes: 1 addition & 1 deletion harmonizer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export type HarmonyRelease = {
language?: Language;
script?: ScriptFrequency;
status?: ReleaseStatus;
types?: Set<ReleaseGroupType>;
types?: ReleaseGroupType[];
releaseDate?: PartialDate;
labels?: Label[];
packaging?: ReleasePackaging;
Expand Down
2 changes: 1 addition & 1 deletion musicbrainz/seeding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion providers/Bandcamp/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ export class BandcampReleaseLookup extends ReleaseLookup<BandcampProvider, Relea
}],
status: 'Official',
packaging: 'None',
types: current.type == 'track' ? new Set(['Single']) : undefined,
types: current.type == 'track' ? ['Single'] : undefined,
externalLinks: [{
url: releaseUrl,
types: linkTypes,
Expand Down
6 changes: 3 additions & 3 deletions providers/iTunes/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,13 +262,13 @@ export class iTunesReleaseLookup extends ReleaseApiLookup<iTunesProvider, Releas
return url;
}

private getTypesFromTitle(title: string): { title: string; types: Set<ReleaseGroupType> } {
private getTypesFromTitle(title: string): { title: string; types: ReleaseGroupType[] } {
const re = /\s- (EP|Single)$/;
const match = title.match(re);
const types = new Set<ReleaseGroupType>();
const types: ReleaseGroupType[] = [];
if (match) {
title = title.replace(re, '');
types.add(match[1] as ReleaseGroupType);
types.push(match[1] as ReleaseGroupType);
}

return { title, types };
Expand Down
4 changes: 2 additions & 2 deletions server/components/Release.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,10 @@ export function Release({ release, releaseMap }: { release: HarmonyRelease; rele
<td>{formatScriptFrequency(script)}</td>
</tr>
)}
{types && types.size > 0 && (
{types && types.length > 0 && (
<tr>
<th>Types</th>
<td>{Array.from(types).join(' + ')}</td>
<td>{types.join(' + ')}</td>
</tr>
)}
{info.providers.length > 1 && (
Expand Down

0 comments on commit 0f08a80

Please sign in to comment.