Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 101 additions & 1 deletion harmonizer/release_types.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
capitalizeReleaseType,
guessDjMixRelease,
guessLiveRelease,
guessTypesForRelease,
guessTypesFromTitle,
Expand All @@ -15,7 +16,7 @@ import type { FunctionSpec } from '../utils/test_spec.ts';

describe('release types', () => {
describe('guess types for release', () => {
const passingCases: Array<[string, HarmonyRelease, string[]]> = [
const passingCases: Array<[string, HarmonyRelease, ReleaseGroupType[]]> = [
['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']],
Expand All @@ -24,6 +25,15 @@ describe('release types', () => {
makeRelease('One Second', undefined, [{ title: 'One Second - Live' }, { title: 'Darker Thoughts - Live' }]),
['Live'],
],
['should detect DJ-mix type from title', makeRelease('DJ-Kicks (Forest Swords) [DJ Mix]'), ['DJ-mix']],
[
'should detect DJ-mix type from tracks',
makeRelease('DJ-Kicks: Modeselektor', undefined, [
{ title: 'PREY - Mixed' },
{ title: 'Permit Riddim - Mixed' },
]),
['DJ-mix'],
],
];

passingCases.forEach(([description, release, expected]) => {
Expand Down Expand Up @@ -123,6 +133,21 @@ describe('release types', () => {
new Set(['Remix']),
])),
['should not treat a premix as remix', 'Wild (premix version)', new Set()],
// DJ Mix releases
...([
'Kitsuné Musique Mixed by YOU LOVE HER (DJ Mix)',
'Club Life - Volume One Las Vegas (Continuous DJ Mix)',
'DJ-Kicks (Forest Swords) [DJ Mix]',
'Paragon Continuous DJ Mix',
'Babylicious (Continuous DJ Mix by Baby Anne)',
].map((
title,
): FunctionSpec<typeof guessTypesFromTitle>[number] => [
`should detect DJ-mix type (${title})`,
title,
new Set(['DJ-mix']),
])),
['should not treat just DJ mix as DJ-mix', 'DJ mix', new Set()],
// Multiple types
[
'should detect both remix and soundtrack type',
Expand Down Expand Up @@ -194,6 +219,81 @@ describe('release types', () => {
});
});

describe('guess DJ-mix release', () => {
const passingCases: FunctionSpec<typeof guessDjMixRelease> = [
['should be true if all tracks have mixed type', [
{
tracklist: [
{ title: 'Heavenly Hell (feat. Ne-Yo) (Mixed)' },
{ title: 'Clap Back (feat. Raphaella) (Mixed)' },
{ title: '2x2 (Mixed)' },
],
},
], true],
['should be true if all tracks on one medium have mixed type', [
{
tracklist: [
{ title: 'PREY - Mixed' },
{ title: 'Permit Riddim - Mixed' },
{ title: 'MEGA MEGA MEGA (DJ-Kicks) - Mixed' },
],
},
{
tracklist: [
{ title: 'PREY' },
{ title: 'Permit Riddim' },
{ title: 'MEGA MEGA MEGA (DJ-Kicks)' },
],
},
], true],
['should support " - Mixed" style', [
{
tracklist: [
{ title: 'Salute - Mixed' },
{ title: 'Friday - Mixed' },
],
},
], true],
['should support case insensitive of mixed', [
{
tracklist: [
{ title: 'Heavenly Hell (feat. Ne-Yo) (mixed)' },
{ title: 'Clap Back (feat. Raphaella) (mixed)' },
{ title: '2x2 (mixed)' },
],
},
], true],
['should support mixed usage of formats', [
{
tracklist: [
{ title: 'Heavenly Hell (feat. Ne-Yo) [Mixed]' },
{ title: 'Clap Back (feat. Raphaella) (Mixed)' },
{ title: '2x2 - Mixed' },
],
},
], true],
['should be false if not all tracks are mixed', [
{
tracklist: [
{ title: 'Heavenly Hell (feat. Ne-Yo) (Mixed)' },
{ title: 'Clap Back (feat. Raphaella)' },
{ title: '2x2 (Mixed)' },
],
},
], false],
['should be false for empty tracklist', [{
tracklist: [],
}], false],
['should be false for no medium', [], false],
];

passingCases.forEach(([description, input, expected]) => {
it(description, () => {
assertEquals(guessDjMixRelease(input), expected);
});
});
});

describe('capitalize release type', () => {
const passingCases: FunctionSpec<typeof capitalizeReleaseType> = [
['should uppercase first letter', 'single', 'Single'],
Expand Down
27 changes: 26 additions & 1 deletion harmonizer/release_types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HarmonyRelease, HarmonyTrack, ReleaseGroupType } from './types.ts';
import { HarmonyMedium, 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. */
Expand All @@ -8,6 +8,9 @@ export function guessTypesForRelease(release: HarmonyRelease): Iterable<ReleaseG
if (!types.has('Live') && guessLiveRelease(release.media.flatMap((media) => media.tracklist))) {
types.add('Live');
}
if (!types.has('DJ-mix') && guessDjMixRelease(release.media)) {
types.add('DJ-mix');
}
return types;
}

Expand All @@ -20,6 +23,8 @@ const releaseGroupTypeMatchers: Array<{ type?: ReleaseGroupType; pattern: RegExp
{ pattern: /\s(EP)(?:\s\(.*?\))?$/i },
// Common remix title: "Remixed", "The Remixes", or "<Track name> (<Remixer> remix)".
{ pattern: /\b(Remix)(?:e[sd])?\b/i },
// Common DJ-mix titles
{ type: 'DJ-mix', pattern: /\bContinuous DJ[\s-]Mix\b|[\(\[]DJ[\s-]mix[\)\]]/i },
// Common soundtrack title: "Official/Original <Medium> Soundtrack" and "Original Score"
{
type: 'Soundtrack',
Expand Down Expand Up @@ -87,6 +92,26 @@ export function guessLiveRelease(tracks: HarmonyTrack[]): boolean {
});
}

/**
* Expression matching common DJ-mix track name patterns.
* Support `Track name - Mixed`, `Track name (Mixed)`, and `Track name [Mixed]`.
*/
const djMixTrackPattern = /\s(?:- Mixed|\(Mixed\)|\[Mixed\])(?:\s\(.*?\))?$/i;

/**
* Returns true if all track titles on at least one medium indicate a DJ-mix release.
*
* Some DJ-mix releases have both a medium with the mixed tracks and another with the unmixed tracks.
*/
export function guessDjMixRelease(media: HarmonyMedium[]): boolean {
return media?.length > 0 && media.some((medium) => {
const tracks = medium.tracklist;
return tracks?.length > 0 && tracks.every((track) => {
return djMixTrackPattern.test(track.title);
});
});
}

/** Takes a release type as a string and turns it into a [ReleaseGroupType]. */
export function capitalizeReleaseType(sourceType: string): ReleaseGroupType {
const type = sourceType.toLowerCase();
Expand Down