-
-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support filtering track titles in albums (#237)
* Add cleanup pattern from track names in scrobbling operations * Use context for cleanup pattern and misc changes --------- Co-authored-by: Enrico Lamperti <[email protected]>
- Loading branch information
1 parent
6233717
commit 51681d4
Showing
12 changed files
with
353 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import { cleanTitleWithPattern, breakStringUsingPattern, strToCleanupPattern } from './CleanupContext'; | ||
|
||
describe('cleanTitleWithPattern', () => { | ||
it('removes pattern from title', () => { | ||
const title = 'Song Title live'; | ||
const pattern = strToCleanupPattern('LIVE'); | ||
expect(cleanTitleWithPattern(title, pattern)).toBe('Song Title'); | ||
}); | ||
|
||
it('returns the original title if pattern is not found', () => { | ||
const title = 'Song Title (live)'; | ||
const pattern = strToCleanupPattern('remix'); | ||
expect(cleanTitleWithPattern(title, pattern)).toBe('Song Title (live)'); | ||
}); | ||
|
||
it('returns the original title if pattern is null', () => { | ||
const title = 'Song Title (2024)'; | ||
expect(cleanTitleWithPattern(title, null)).toBe('Song Title (2024)'); | ||
}); | ||
|
||
it('skips patterns of less than 3 characters', () => { | ||
const title = 'Song Title (20)'; | ||
const pattern = strToCleanupPattern('20'); | ||
expect(cleanTitleWithPattern(title, pattern)).toBe('Song Title (20)'); | ||
}); | ||
|
||
it('removes trailing dashes', () => { | ||
const title = 'Song Title - Live'; | ||
const pattern = strToCleanupPattern('live'); | ||
expect(cleanTitleWithPattern(title, pattern)).toBe('Song Title'); | ||
}); | ||
|
||
it('removes empty parenthesis', () => { | ||
const title = 'Song Title (remix)'; | ||
const pattern = strToCleanupPattern('remix'); | ||
expect(cleanTitleWithPattern(title, pattern)).toBe('Song Title'); | ||
}); | ||
|
||
it('removes empty brackets', () => { | ||
const title = 'Song Title [fOoB4r]'; | ||
const pattern = strToCleanupPattern('fOoB4r'); | ||
expect(cleanTitleWithPattern(title, pattern)).toBe('Song Title'); | ||
}); | ||
|
||
it("doesn't match partial end of words", () => { | ||
const title = 'Song Title remix'; | ||
const pattern = strToCleanupPattern('mix'); | ||
expect(cleanTitleWithPattern(title, pattern)).toBe('Song Title remix'); | ||
}); | ||
|
||
it("doesn't match partial beginning of words", () => { | ||
const title = 'Love (Sweet Love) (Dopamine Remix)'; | ||
const pattern = strToCleanupPattern('dopa'); | ||
expect(cleanTitleWithPattern(title, pattern)).toBe(title); | ||
}); | ||
|
||
it('auto-closes parenthesis', () => { | ||
const title = 'Song Title (remix) [1984]'; | ||
const pattern = strToCleanupPattern('(remix'); | ||
expect(cleanTitleWithPattern(title, pattern)).toBe('Song Title [1984]'); | ||
}); | ||
|
||
it('auto-closes brackets', () => { | ||
const title = 'Song Title (remix) [1984]'; | ||
const pattern = strToCleanupPattern('[1984'); | ||
expect(cleanTitleWithPattern(title, pattern)).toBe('Song Title (remix)'); | ||
}); | ||
|
||
it('matches words starting with symbols', () => { | ||
const title = 'Song Title $remix'; | ||
const pattern = strToCleanupPattern('$remix'); | ||
expect(cleanTitleWithPattern(title, pattern)).toBe('Song Title'); | ||
}); | ||
|
||
it('supports several words in the pattern', () => { | ||
const title = "I can't quit you baby - Live in paris, 1969"; | ||
const pattern = strToCleanupPattern('live in paris, 1969'); | ||
expect(cleanTitleWithPattern(title, pattern)).toBe("I can't quit you baby"); | ||
}); | ||
}); | ||
|
||
describe('breakStringUsingPattern', () => { | ||
it('breaks down the string into parts matching the pattern', () => { | ||
const str = 'Song Title (remix) (live) (2024)'; | ||
const pattern = /\(remix\)|\(live\)|\(2024\)/gi; | ||
const result = breakStringUsingPattern(str, pattern); | ||
expect(result).toEqual([ | ||
{ value: 'Song Title', isMatch: false }, | ||
{ value: ' (remix)', isMatch: true }, | ||
{ value: ' (live)', isMatch: true }, | ||
{ value: ' (2024)', isMatch: true }, | ||
]); | ||
}); | ||
|
||
it('marks extra spaces', () => { | ||
const str = 'Song Title (remix) - More'; | ||
const pattern = /\(remix\)/gi; | ||
const result = breakStringUsingPattern(str, pattern); | ||
expect(result).toEqual([ | ||
{ value: 'Song Title', isMatch: false }, | ||
{ value: ' (remix)', isMatch: true }, | ||
{ value: ' - More', isMatch: false }, | ||
]); | ||
}); | ||
|
||
it('returns the whole string as non-matching if pattern is not found', () => { | ||
const str = 'Song Title (remix)'; | ||
const pattern = /\(live\)/gi; | ||
const result = breakStringUsingPattern(str, pattern); | ||
expect(result).toEqual([{ value: 'Song Title (remix)', isMatch: false }]); | ||
}); | ||
|
||
it('returns the whole string as non-matching if pattern is null', () => { | ||
const str = 'Song Title (2024)'; | ||
const result = breakStringUsingPattern(str, null); | ||
expect(result).toEqual([{ value: 'Song Title (2024)', isMatch: false }]); | ||
}); | ||
|
||
it('returns an empty array if the string is empty', () => { | ||
const str = ''; | ||
const pattern = /\(remix\)/gi; | ||
const result = breakStringUsingPattern(str, pattern); | ||
expect(result).toEqual([]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import { createContext } from 'react'; | ||
import { escapeRegExp } from 'lodash-es'; | ||
|
||
export type CleanupContext = { | ||
// this type represents the content of the context, it's a regexp value (cleanupPattern) and the corresponidng state setter (setCleanupPattern) | ||
cleanupPattern: RegExp | null; | ||
setCleanupPattern: (pattern?: RegExp) => void; | ||
}; | ||
|
||
type BreakStringResult = { | ||
value: string; | ||
isMatch: boolean; | ||
}; | ||
|
||
export const CleanupPatternContext = createContext<CleanupContext>({ | ||
cleanupPattern: null, | ||
setCleanupPattern: () => {}, | ||
}); | ||
|
||
export function cleanTitleWithPattern(title: string, pattern: RegExp): string { | ||
if (!pattern) return title; | ||
|
||
return breakStringUsingPattern(title, pattern) | ||
.filter(({ isMatch }) => !isMatch) | ||
.reduce((acc, { value }) => acc + value, '') | ||
.trim(); | ||
} | ||
|
||
/** | ||
* Converts a string into a regular expression, to be used as cleanup pattern | ||
* @param str string to be converted to pattern | ||
* @returns regular expression using the given string | ||
*/ | ||
export function strToCleanupPattern(str: string): RegExp { | ||
if (!str || str.trim().length < 3) return; | ||
const startsWithWord = str.match(/^\w/); | ||
const prefix = startsWithWord ? '\\s?\\b' : ''; // disallow partial words | ||
|
||
let postfix = ''; | ||
if (startsWithWord) { | ||
// disallow partial words | ||
postfix = '\\s?\\b'; | ||
} else if (str.match(/^[([]/)) { | ||
// auto-close parenthesis and brackets | ||
postfix = '[\\)\\]]?'; | ||
} | ||
return new RegExp(`${prefix}${escapeRegExp(str)}${postfix}`, 'ig'); | ||
} | ||
|
||
/** | ||
* Breaks down the given string into pieces that match (or don't match) the given pattern | ||
* @param str string to break down into parts | ||
* @param pattern pattern to use to break down the string | ||
*/ | ||
export function breakStringUsingPattern(str: string, pattern: RegExp): BreakStringResult[] { | ||
if (!str) return []; | ||
|
||
const __stringCrumble = (value: string, isMatch: boolean): BreakStringResult => ({ | ||
value, | ||
isMatch, | ||
}); | ||
|
||
if (!pattern) return [__stringCrumble(str, false)]; | ||
|
||
const matches = str.matchAll(pattern); | ||
|
||
if (!matches) return [__stringCrumble(str, false)]; | ||
|
||
const crumbles = [] as BreakStringResult[]; | ||
const splitStr = str.split(pattern); | ||
|
||
splitStr.forEach((part, index) => { | ||
// This block checks interactions between valid parts for invalid combinations | ||
// (extra spaces, empty parenthesis/brackets, more than one dash) | ||
if (index > 0) { | ||
let previous = splitStr[index - 1]; | ||
|
||
if (previous.match(/ +$/) && part.match(/^(\s|$)/)) { | ||
// modify the previous crumble to remove the space | ||
previous = previous.trimEnd(); | ||
crumbles[crumbles.length - 2].value = previous; | ||
// and add the space to the corresponding match | ||
crumbles[crumbles.length - 1].value = ' ' + crumbles[crumbles.length - 1].value; | ||
} | ||
|
||
if (previous.endsWith('(') && part.startsWith(')')) { | ||
// modify the previous crumble to remove the empty parenthesis | ||
crumbles[crumbles.length - 2].value = previous.slice(0, -1); | ||
// add it to the corresponding match | ||
crumbles[crumbles.length - 1].value = `(${crumbles[crumbles.length - 1].value})`; | ||
// and remove it from the part we're currently processing | ||
part = part.slice(1); | ||
} | ||
|
||
if (previous.endsWith('[') && part.startsWith(']')) { | ||
// modify the previous crumble to remove the empty brackets | ||
crumbles[crumbles.length - 2].value = previous.slice(0, -1); | ||
// add it to the corresponding match | ||
crumbles[crumbles.length - 1].value = `[${crumbles[crumbles.length - 1].value}]`; | ||
// and remove it from the part we're currently processing | ||
part = part.slice(1); | ||
} | ||
|
||
if (previous.match(/[-–—]$/) && (!part || part.match(/^\s*[-–—]/))) { | ||
const removedChar = previous.at(-1); | ||
// modify the previous crumble to remove the extra dash | ||
crumbles[crumbles.length - 2].value = previous.slice(0, -1); | ||
// and add it to the corresponding match | ||
crumbles[crumbles.length - 1].value = removedChar + crumbles[crumbles.length - 1].value; | ||
} | ||
} | ||
|
||
// Then we add the current part to the crumbles array | ||
crumbles.push(__stringCrumble(part, false)); | ||
|
||
// And its corresponding match, if there is one | ||
const match = matches.next(); | ||
if (match.done) return; | ||
crumbles.push(__stringCrumble(match.value[0], true)); | ||
}); | ||
|
||
return crumbles.filter((crumble) => crumble.value); | ||
} |
Oops, something went wrong.