Skip to content

Commit

Permalink
Parse song info from Last.fm url (#251)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewarosario authored Sep 11, 2024
1 parent 1d3cf54 commit 6233717
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 29 deletions.
127 changes: 99 additions & 28 deletions src/domains/scrobbleSong/SongForm.test.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,110 @@
import { extractArtistTitle } from './SongForm';

describe('`extractArtistTitle` helper', () => {
it('extracts artist and title from text', () => {
const text = 'Artist - Title';
const result = extractArtistTitle(text);
expect(result).toEqual({ artist: 'Artist', title: 'Title' });
});
describe('from pasted text', () => {
it('extracts artist and title from text', () => {
const text = 'Artist - Title';
const result = extractArtistTitle(text);
expect(result).toEqual({ artist: 'Artist', title: 'Title' });
});

it('extracts artist and title from text in reverse order', () => {
const text = 'Title - Artist';
const result = extractArtistTitle(text, true);
expect(result).toEqual({ artist: 'Artist', title: 'Title' });
});
it('extracts artist and title reversed', () => {
// This is the case where the user pastes into the title field
const text = 'Title - Artist';
const result = extractArtistTitle(text, true);
expect(result).toEqual({ artist: 'Artist', title: 'Title' });
});

it('returns null if text does not match pattern', () => {
const text = 'Invalid text';
const result = extractArtistTitle(text);
expect(result).toBeNull();
});
it('returns null if text does not match pattern', () => {
const text = 'Invalid text';
const result = extractArtistTitle(text);
expect(result).toBeNull();
});

it('works with emdash', () => {
const text = 'Artist — Title';
const result = extractArtistTitle(text);
expect(result).toEqual({ artist: 'Artist', title: 'Title' });
});
it('works with emdash', () => {
const text = 'Artist — Title';
const result = extractArtistTitle(text);
expect(result).toEqual({ artist: 'Artist', title: 'Title' });
});

it('works with endash', () => {
const text = 'Artist – Title';
const result = extractArtistTitle(text);
expect(result).toEqual({ artist: 'Artist', title: 'Title' });
it('works with endash', () => {
const text = 'Artist – Title';
const result = extractArtistTitle(text);
expect(result).toEqual({ artist: 'Artist', title: 'Title' });
});

it('works with emdash even without spaces around it', () => {
const text = 'JAY-Z—4:44';
const result = extractArtistTitle(text);
expect(result).toEqual({ artist: 'JAY-Z', title: '4:44' });
});
});

it('works with emdash even without spaces around it', () => {
const text = 'JAY-Z—4:44';
const result = extractArtistTitle(text);
expect(result).toEqual({ artist: 'JAY-Z', title: '4:44' });
describe('from Last.fm URLs', () => {
it('extracts artist and title', () => {
const url = 'https://www.last.fm/music/Artist/_/Title';
const result = extractArtistTitle(url);
expect(result).toEqual({ artist: 'Artist', title: 'Title' });
});

it('supports a partial URL', () => {
const url = 'last.fm/music/Artist/_/Title';
const result = extractArtistTitle(url);
expect(result).toEqual({ artist: 'Artist', title: 'Title' });
});

it('skips country code', () => {
const url = 'https://www.last.fm/pt/music/Artist/_/Title';
const result = extractArtistTitle(url);
expect(result).toEqual({ artist: 'Artist', title: 'Title' });
});

it('skips country code in albums', () => {
const url = 'https://www.last.fm/fr/music/Artist/Album/';
const result = extractArtistTitle(url);
expect(result).toEqual(null);
});

it('extracts the album name if present', () => {
const url = 'https://www.last.fm/music/Artist/Album/Title';
const result = extractArtistTitle(url);
expect(result).toEqual({ artist: 'Artist', album: 'Album', title: 'Title' });
});

it('handles encoded characters', () => {
const url = 'https://www.last.fm/music/Artist%20Name/_/Title%20Name';
const result = extractArtistTitle(url);
expect(result).toEqual({ artist: 'Artist Name', title: 'Title Name' });
});

it('extracts artist and title with encoded characters and special characters', () => {
const url = 'https://www.last.fm/music/Artist%20Name/_/Title%20Name%20(Feat.%20Artist%202)';
const result = extractArtistTitle(url);
expect(result).toEqual({ artist: 'Artist Name', title: 'Title Name (Feat. Artist 2)' });
});

it.skip('supports a double-encoded plus sign', () => {
// We shouldn't get cases like this, but I happened to find this one. No use in fixing it for now.
const url = 'https://www.last.fm/es/music/Florence+%252B+the+Machine/_/You%27ve+Got+the+Love';
const result = extractArtistTitle(url);
expect(result).toEqual({ artist: 'Florence + the Machine', title: "You've Got the Love" });
});

it('handles question marks properly', () => {
const url =
'https://www.last.fm/es/music/Man%C3%A1/%C2%BFD%C3%B3nde+jugar%C3%A1n+los+ni%C3%B1os%3F/%C2%BFD%C3%B3nde+jugar%C3%A1n+los+ni%C3%B1os%3F';
const result = extractArtistTitle(url);
expect(result).toEqual({
artist: 'Maná',
album: '¿Dónde jugarán los niños?',
title: '¿Dónde jugarán los niños?',
});
});

it('returns null if URL does not match pattern', () => {
const url = 'https://www.last.fm/music/Artist';
const result = extractArtistTitle(url);
expect(result).toBeNull();
});
});
});
30 changes: 29 additions & 1 deletion src/domains/scrobbleSong/SongForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ const Tooltip = lazyWithPreload(() => import('components/Tooltip'));
const reAutoPasteSplitting = / - | ?[–—] ?/;
const controlOrder = ['artist', 'title', 'album']; // Used for arrow navigation

export function extractArtistTitle(text: string, reverse = false) {
type SongMatch = {
artist: string;
title: string;
album?: string;
} | null;

function splitArtistTitleFromText(text: string, reverse: boolean): SongMatch {
if (reAutoPasteSplitting.test(text)) {
const result = text.split(reAutoPasteSplitting, 2);

Expand All @@ -44,6 +50,25 @@ export function extractArtistTitle(text: string, reverse = false) {
return null;
}

const reLastfmURL = /last\.fm(?:\/[a-zA-Z]{2})?\/music\/([^/]+)\/([^/]+?)\/([^/]+)/;
function parseLastFmUrl(url: string): SongMatch {
const match = url.match(reLastfmURL);

if (!match) {
return null;
}

return {
artist: decodeURIComponent(match[1].replace(/\+/g, ' ')),
album: match[2] !== '_' ? decodeURIComponent(match[2].replace(/\+/g, ' ')) : undefined,
title: decodeURIComponent(match[3].replace(/\+/g, ' ')),
};
}

export function extractArtistTitle(text: string, reverse = false): SongMatch {
return parseLastFmUrl(text) ?? splitArtistTitleFromText(text, reverse);
}

export function SongForm() {
const [album, setAlbum] = useState('');
const [artist, setArtist] = useState('');
Expand Down Expand Up @@ -133,6 +158,9 @@ export function SongForm() {

setArtist(splittedValues.artist);
setTitle(splittedValues.title);
if (splittedValues.album) {
setAlbum(splittedValues.album);
}

const undoPaste = () => {
cloneDataFromScrobble(prevState);
Expand Down

0 comments on commit 6233717

Please sign in to comment.