Skip to content

Commit 7289e99

Browse files
committed
feat: Detect DJ-mix based on the release title or track titles
1 parent 4bd1630 commit 7289e99

File tree

2 files changed

+127
-2
lines changed

2 files changed

+127
-2
lines changed

harmonizer/release_types.test.ts

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
capitalizeReleaseType,
3+
guessDjMixRelease,
34
guessLiveRelease,
45
guessTypesForRelease,
56
guessTypesFromTitle,
@@ -15,7 +16,7 @@ import type { FunctionSpec } from '../utils/test_spec.ts';
1516

1617
describe('release types', () => {
1718
describe('guess types for release', () => {
18-
const passingCases: Array<[string, HarmonyRelease, string[]]> = [
19+
const passingCases: Array<[string, HarmonyRelease, ReleaseGroupType[]]> = [
1920
['should detect EP type from title', makeRelease('Wake of a Nation (EP)'), ['EP']],
2021
['should keep existing types', makeRelease('Wake of a Nation (EP)', ['Interview']), ['EP', 'Interview']],
2122
['should detect live type from title', makeRelease('One Second (Live)'), ['Live']],
@@ -24,6 +25,15 @@ describe('release types', () => {
2425
makeRelease('One Second', undefined, [{ title: 'One Second - Live' }, { title: 'Darker Thoughts - Live' }]),
2526
['Live'],
2627
],
28+
['should detect DJ-mix type from title', makeRelease('DJ-Kicks (Forest Swords) [DJ Mix]'), ['DJ-mix']],
29+
[
30+
'should detect DJ-mix type from tracks',
31+
makeRelease('DJ-Kicks: Modeselektor', undefined, [
32+
{ title: 'PREY - Mixed' },
33+
{ title: 'Permit Riddim - Mixed' },
34+
]),
35+
['DJ-mix'],
36+
],
2737
];
2838

2939
passingCases.forEach(([description, release, expected]) => {
@@ -123,6 +133,21 @@ describe('release types', () => {
123133
new Set(['Remix']),
124134
])),
125135
['should not treat a premix as remix', 'Wild (premix version)', new Set()],
136+
// DJ Mix releases
137+
...([
138+
'Kitsuné Musique Mixed by YOU LOVE HER (DJ Mix)',
139+
'Club Life - Volume One Las Vegas (Continuous DJ Mix)',
140+
'DJ-Kicks (Forest Swords) [DJ Mix]',
141+
'Paragon Continuous DJ Mix',
142+
'Babylicious (Continuous DJ Mix by Baby Anne)',
143+
].map((
144+
title,
145+
): FunctionSpec<typeof guessTypesFromTitle>[number] => [
146+
`should detect DJ-mix type (${title})`,
147+
title,
148+
new Set(['DJ-mix']),
149+
])),
150+
['should not treat just DJ mix as DJ-mix', 'DJ mix', new Set()],
126151
// Multiple types
127152
[
128153
'should detect both remix and soundtrack type',
@@ -194,6 +219,81 @@ describe('release types', () => {
194219
});
195220
});
196221

222+
describe('guess DJ-mix release', () => {
223+
const passingCases: FunctionSpec<typeof guessDjMixRelease> = [
224+
['should be true if all tracks have mixed type', [
225+
{
226+
tracklist: [
227+
{ title: 'Heavenly Hell (feat. Ne-Yo) (Mixed)' },
228+
{ title: 'Clap Back (feat. Raphaella) (Mixed)' },
229+
{ title: '2x2 (Mixed)' },
230+
],
231+
},
232+
], true],
233+
['should be true if all tracks on one medium have mixed type', [
234+
{
235+
tracklist: [
236+
{ title: 'PREY - Mixed' },
237+
{ title: 'Permit Riddim - Mixed' },
238+
{ title: 'MEGA MEGA MEGA (DJ-Kicks) - Mixed' },
239+
],
240+
},
241+
{
242+
tracklist: [
243+
{ title: 'PREY' },
244+
{ title: 'Permit Riddim' },
245+
{ title: 'MEGA MEGA MEGA (DJ-Kicks)' },
246+
],
247+
},
248+
], true],
249+
['should support " - Mixed" style', [
250+
{
251+
tracklist: [
252+
{ title: 'Salute - Mixed' },
253+
{ title: 'Friday - Mixed' },
254+
],
255+
},
256+
], true],
257+
['should support case insensitive of mixed', [
258+
{
259+
tracklist: [
260+
{ title: 'Heavenly Hell (feat. Ne-Yo) (mixed)' },
261+
{ title: 'Clap Back (feat. Raphaella) (mixed)' },
262+
{ title: '2x2 (mixed)' },
263+
],
264+
},
265+
], true],
266+
['should support mixed usage of formats', [
267+
{
268+
tracklist: [
269+
{ title: 'Heavenly Hell (feat. Ne-Yo) [Mixed]' },
270+
{ title: 'Clap Back (feat. Raphaella) (Mixed)' },
271+
{ title: '2x2 - Mixed' },
272+
],
273+
},
274+
], true],
275+
['should be false if not all tracks are mixed', [
276+
{
277+
tracklist: [
278+
{ title: 'Heavenly Hell (feat. Ne-Yo) (Mixed)' },
279+
{ title: 'Clap Back (feat. Raphaella)' },
280+
{ title: '2x2 (Mixed)' },
281+
],
282+
},
283+
], false],
284+
['should be false for empty tracklist', [{
285+
tracklist: [],
286+
}], false],
287+
['should be false for no medium', [], false],
288+
];
289+
290+
passingCases.forEach(([description, input, expected]) => {
291+
it(description, () => {
292+
assertEquals(guessDjMixRelease(input), expected);
293+
});
294+
});
295+
});
296+
197297
describe('capitalize release type', () => {
198298
const passingCases: FunctionSpec<typeof capitalizeReleaseType> = [
199299
['should uppercase first letter', 'single', 'Single'],

harmonizer/release_types.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { HarmonyRelease, HarmonyTrack, ReleaseGroupType } from './types.ts';
1+
import { HarmonyMedium, HarmonyRelease, HarmonyTrack, ReleaseGroupType } from './types.ts';
22
import { primaryTypeIds } from '@kellnerd/musicbrainz/data/release-group';
33

44
/** Guess the types for a release from release and track titles. */
@@ -8,6 +8,9 @@ export function guessTypesForRelease(release: HarmonyRelease): Iterable<ReleaseG
88
if (!types.has('Live') && guessLiveRelease(release.media.flatMap((media) => media.tracklist))) {
99
types.add('Live');
1010
}
11+
if (!types.has('DJ-mix') && guessDjMixRelease(release.media)) {
12+
types.add('DJ-mix');
13+
}
1114
return types;
1215
}
1316

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

95+
/**
96+
* Expression matching common DJ-mix track name patterns.
97+
* Support `Track name - Mixed`, `Track name (Mixed)`, and `Track name [Mixed]`.
98+
*/
99+
const djMixTrackPattern = /\s(?:- Mixed|\(Mixed\)|\[Mixed\])(?:\s\(.*?\))?$/i;
100+
101+
/**
102+
* Returns true if all track titles on at least one medium indicate a DJ-mix release.
103+
*
104+
* Some DJ-mix releases have both a medium with the mixed tracks and another with the unmixed tracks.
105+
*/
106+
export function guessDjMixRelease(media: HarmonyMedium[]): boolean {
107+
return media?.length > 0 && media.some((medium) => {
108+
const tracks = medium.tracklist;
109+
return tracks?.length > 0 && tracks.every((track) => {
110+
return djMixTrackPattern.test(track.title);
111+
});
112+
});
113+
}
114+
90115
/** Takes a release type as a string and turns it into a [ReleaseGroupType]. */
91116
export function capitalizeReleaseType(sourceType: string): ReleaseGroupType {
92117
const type = sourceType.toLowerCase();

0 commit comments

Comments
 (0)