Skip to content

Commit

Permalink
Import from Apple Music (#25)
Browse files Browse the repository at this point in the history
Importing a tracklist from Apple Music is as easy as pasting the album
URL into a new "Import From..." model. I have started with Apple
Music, but I will add more soon.

The Tracklist Editor still works entirely offline if you don't use the
import feature. However, due to CORS the import feature cannot be part
of the static web page. I will have to manually push changes to the
importer. If that becomes a pain I can automate it with CD later.

One known issue: If the album is semi-various artists, the importer will
only catch a few tracks. Semi-various artists is a thing with
Apple Music where albums that contain mostly one artist do not show that
artist on those tracks. This causes havoc with the (probably brittle)
regex that parses the raw HTML.
  • Loading branch information
elliotchance authored Dec 29, 2021
1 parent b6fa988 commit e514040
Show file tree
Hide file tree
Showing 10 changed files with 473 additions and 2 deletions.
2 changes: 2 additions & 0 deletions import/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.serverless
node_modules
6 changes: 6 additions & 0 deletions import/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# package directories
node_modules
jspm_packages

# Serverless directories
.serverless
8 changes: 8 additions & 0 deletions import/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
test:
node apple-music.test.js

dev:
sls deploy --stage dev

prod: test
sls deploy --stage production
22 changes: 22 additions & 0 deletions import/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Some of these instructions are mostly for my own benefit, but if you wanted to
deploy your own copy for debugging or any other reason, go for it.

# Run Tests

```sh
make test
```

# Deploy

Development:

```bash
make dev
```

Production (will run tests first):

```bash
make prod
```
72 changes: 72 additions & 0 deletions import/apple-music.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use strict';

// Examples:
// sls invoke -f apple-music -d '{"url":"https://music.apple.com/us/album/nye-2022-dj-mix/1600990821"}'
// curl "https://sxxqp2lco7.execute-api.us-east-1.amazonaws.com/dev/apple-music?url=https%3A%2F%2Fmusic.apple.com%2Fus%2Falbum%2Fnye-2022-dj-mix%2F1600990821"

const fetch = require('node-fetch');

module.exports.import = async (event) => {
// Validate the URL to prevent mistakes and abuse.
const querystring = event.queryStringParameters;
const url = querystring.url;
if (!url.startsWith('https://music.apple.com/us/album/')) {
return response(400, {
error: `Invalid URL: ${url}`,
});
}

// Fetch the raw HTML.
const resp = await fetch(url);
const body = await resp.text();

// Parse tracks.
//
// TODO(elliotchance): This is an ultra crude regexp that will probably break
// in the future.
//
// There are a few layouts we need to test, specially various artist albums
// have a different layout than a single artist album.

// If we get more than one match, this must be a VA album. The regexp happens
// to fall out that way, but also an album with one track could hardly be
// considered various artists. Maybe this is possible - I haven't seen any
// examples of this though.
let matches = Array.from(body.matchAll(/songs-list-row__song-name">([^<]+).*?row__link".*?>([^<]+).*?row__length">([^<]+)/gs));
let decoder = (match, number) => ({
number,
title: match[2].trim() + ' - ' + match[1].trim(),
time: match[3].trim(),
});

if (matches.length < 2) {
matches = Array.from(body.matchAll(/songs-list-row__song-name".*?>(.*?)<.*?row__length".*?>(.*?)</gs));
decoder = (match, number) => ({
number,
title: match[1].trim(),
time: match[2].trim(),
});
}

let tracks = [];
let number = 1;
for (const match of matches) {
tracks.push(decoder(match, number));
++number;
}

return response(200, {
tracks,
});
};

function response(statusCode, body) {
return {
statusCode,
body: JSON.stringify(body),
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true,
},
};
}
200 changes: 200 additions & 0 deletions import/apple-music.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
const importer = require('./apple-music').import;
const assert = require('assert');

function assertAppleMusic(tt) {
(async () => {
const resp = JSON.parse((await importer({
queryStringParameters: {
url: tt.url,
}
})).body);

if (tt.error || resp.error) {
assert.equal(resp.error, tt.error);
return;
}

assert.deepEqual(resp.tracks, tt.tracks);
})();
}

// Bad URL
assertAppleMusic({
url: 'https://notapple.com/1600990821',
error: 'Invalid URL: https://notapple.com/1600990821',
});

// Single track
assertAppleMusic({
url: 'https://music.apple.com/us/album/go-on-then-love-feat-the-maine-odea-remix-odea-remix-single/1591618917',
tracks: [
{
number: 1,
title: 'Go On Then, Love (feat. The Maine) [Odea Remix]',
time: '3:08'
}
],
});

// Single artist
assertAppleMusic({
url: 'https://music.apple.com/us/album/dreaming-out-loud/1440889649',
tracks: [
{ number: 1, title: 'Say (All I Need)', time: '3:50' },
{ number: 2, title: 'Mercy', time: '4:01' },
{ number: 3, title: 'Stop and Stare', time: '3:44' },
{ number: 4, title: 'Apologize', time: '3:25' },
{ number: 5, title: 'Goodbye, Apathy', time: '3:32' },
{ number: 6, title: 'All Fall Down', time: '4:06' },
{ number: 7, title: 'Tyrant', time: '5:05' },
{ number: 8, title: 'Prodigal', time: '3:56' },
{ number: 9, title: "Won't Stop", time: '5:05' },
{ number: 10, title: 'All We Are', time: '4:23' },
{ number: 11, title: 'Someone to Save You', time: '4:13' },
{ number: 12, title: 'Come Home', time: '4:29' },
{
number: 13,
title: 'Apologize (feat. OneRepublic)',
time: '3:05'
}
],
});

// Various artists
assertAppleMusic({
url: 'https://music.apple.com/us/album/nye-2022-dj-mix/1600990821',
tracks: [
{
number: 1,
title: 'Westside Gunn - Praise God Intro (feat. AA Rashid) [Mixed]',
time: '2:03'
},
{
number: 2,
title: "The S.S.O. Orchestra - Tonight's the Night (feat. Douglas Lucas &amp; The Sugar Sisters) [Mixed]",
time: '1:11'
},
{
number: 3,
title: 'Irish Coffee - The Show, Pt. 1 / Scrabble (Mixed)',
time: '1:42'
},
{
number: 4,
title: "All The People - Cramp Your Style / Don't Play No Game That I Can't Win (feat. Santigold) [Remix] [Mixed]",
time: '1:21'
},
{ number: 5, title: 'Nas - Nasty (Mixed)', time: '1:01' },
{
number: 6,
title: 'Johnny Pate - Shaft In Africa (Addis) [Mixed]',
time: '1:19'
},
{
number: 7,
title: 'Freedom - Get Up and Dance (Mixed)',
time: '1:12'
},
{
number: 8,
title: "Funky 4+1 - That's the Joint (Mixed)",
time: '1:00'
},
{
number: 9,
title: 'Esther Williams - Last Night Changed It All (I Really Had a Ball) [Mixed]',
time: '1:06'
},
{
number: 10,
title: "Jungle Brothers - Feelin' Alright / Love Vibration (Mixed)",
time: '0:51'
},
{
number: 11,
title: 'Indeep - Last Night a D.J. Saved My Life / Agboju Logun (Mr Bongo 7" Edit) [Mixed]',
time: '1:50'
},
{
number: 12,
title: 'Bobby Thurston - You Got What It Takes (Remix) [Mixed]',
time: '2:18'
},
{
number: 13,
title: 'The Rebirth - Evil Vibrations (Remix) [Mixed]',
time: '1:33'
},
{
number: 14,
title: 'Vaughan Mason and Crew - Bounce, Rock, Skate, Roll (Mixed)',
time: '3:24'
},
{
number: 15,
title: 'Love Unlimited - I Did It for Love (Mixed)',
time: '1:59'
},
{
number: 16,
title: 'King Sporty &amp; The Root Rockers - Get on Down (Mixed)',
time: '1:20'
},
{
number: 17,
title: 'El Turronero - Las Penas (Las Cañas) [Mixed]',
time: '2:06'
},
{
number: 18,
title: "Sault - Don't Waste My Time (Mixed)",
time: '1:47'
},
{
number: 19,
title: 'Can - Vitamin C (U.N.K.L.E. Mix) [Mixed]',
time: '3:24'
},
{
number: 20,
title: 'Radiohead - Everything In Its Right Place (Mixed)',
time: '3:54'
},
{
number: 21,
title: 'ID - ID1 (from NYE 2022: Zane Lowe) [Mixed]',
time: '2:47'
},
{
number: 22,
title: 'Pete Le Freq - One More Step (Mixed)',
time: '3:17'
},
{
number: 23,
title: 'ID - ID2 (from NYE 2022: Zane Lowe) [Mixed]',
time: '4:10'
},
{ number: 24, title: 'iZNiiK - Leave (Mixed)', time: '2:27' },
{
number: 25,
title: 'Disclosure - In My Arms (Mixed)',
time: '2:09'
},
{ number: 26, title: 'SG Lewis - Time (Mixed)', time: '2:28' },
{
number: 27,
title: 'Underworld - Two Months Off (Mixed)',
time: '5:48'
}
],
});

// TODO(elliotchance): These ones do not working.

// Various artists with mostly the same artist (so this they don't show on most
// of the tracks)
// assertAppleMusic({
// url: 'https://music.apple.com/us/album/mirage/398383005',
// tracks: [],
// });
71 changes: 71 additions & 0 deletions import/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions import/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"node-fetch": "^2.6.6"
}
}
Loading

0 comments on commit e514040

Please sign in to comment.