feat(Apple Music): add support for song-type URLs (#552)

This commit is contained in:
Miraculous Owonubi 2023-08-08 04:44:28 +01:00 committed by GitHub
parent ed4fedfbdd
commit 8c57292441
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 116 additions and 23 deletions

View File

@ -952,14 +952,22 @@ To preview filter rules specification, use the `filter` subcommand.
<td> <code> spotify:playlist:37i9dQZF1DXcBWIGoYBM5M </code> </td>
</tr>
<tr>
<td rowspan=8> Apple Music </td>
<td rowspan=2> track </td>
<td rowspan=10> Apple Music </td>
<td rowspan=4> track </td>
<td> URL </td>
<td> <a href="https://music.apple.com/us/album/say-so-feat-nicki-minaj/1510821672?i=1510821685"> https://music.apple.com/us/album/say-so-feat-nicki-minaj/1510821672?i=1510821685 </a> </td>
</tr>
<tr>
<td> URI </td>
<td> <code> apple_music:track:1510821672i1510821685 </code> </td>
<td> <code> apple_music:track:1510821685 </code> </td>
</tr>
<tr>
<td> URL </td>
<td> <a href="https://music.apple.com/us/song/1510821685"> https://music.apple.com/us/song/1510821685 </a> </td>
</tr>
<tr>
<td> URI </td>
<td> <code> apple_music:track:1510821685 </code> </td>
</tr>
<tr>
<td rowspan=2> album </td>

2
cli.js
View File

@ -2169,7 +2169,7 @@ function prepCli(packageJson) {
console.log(' spotify:album:2D23kwwoy2JpZVuJwzE42B');
console.log('');
console.log(' $ freyr urify -t https://music.apple.com/us/album/say-so-feat-nicki-minaj/1510821672?i=1510821685');
console.log(' apple_music:track:1510821672i1510821685');
console.log(' apple_music:track:1510821685');
console.log('');
console.log(
[

View File

@ -19,9 +19,9 @@ export default class AppleMusic {
isSearchable: false,
isSourceable: false,
},
// https://www.debuggex.com/r/nbRgm3fyDn2oampX
// https://www.debuggex.com/r/Pv_Prjinkz1m2FOB
VALID_URL:
/(?:(?:(?:(?:https?:\/\/)?(?:www\.)?)(?:(?:music|(?:geo\.itunes))\.apple.com)\/([a-z]{2})\/(album|artist|playlist)\/(?:([^/]+)\/)?\w+)|(?:apple_music:(track|album|artist|playlist):([\w.]+)))/,
/(?:(?:(?:(?:https?:\/\/)?(?:www\.)?)(?:(?:music|(?:geo\.itunes))\.apple.com)\/([a-z]{2})\/(song|album|artist|playlist)\/(?:([^/]+)\/)?\w+)|(?:apple_music:(track|album|artist|playlist):([\w.]+)))/,
PROP_SCHEMA: {},
};
@ -90,20 +90,17 @@ export default class AppleMusic {
if (!match) return null;
const isURI = !!match[4];
const parsedURL = xurl.parse(uri, true);
let collection_type = match[isURI ? 4 : 2];
let id = (isURI && match[4] === 'track' ? match[5] : parsedURL.query.i) || null;
const type = isURI ? match[4] : collection_type === 'album' && id ? 'track' : collection_type;
collection_type = type === 'track' && !id ? 'album' : collection_type;
let refID = isURI ? (type !== 'track' ? match[5] : null) : path.basename(parsedURL.pathname);
if (type === 'track' && !refID) if (id.match(/^(\d+)i(\d+)$/)) [refID, id] = id.split('i');
storefront = match[1] || storefront || (#store in this && this.#store.defaultStorefront) || 'us';
const collection_type = isURI ? match[4] : match[2] === 'song' ? 'track' : match[2];
const id = isURI ? match[5] : parsedURL.query.i || path.basename(parsedURL.pathname);
const type = isURI ? match[4] : collection_type == 'album' && parsedURL.query.i ? 'track' : collection_type;
const scope = collection_type == 'track' || (collection_type == 'album' && parsedURL.query.i) ? 'song' : collection_type;
storefront = match[1] || storefront || (#store in this ? this.#store.defaultStorefront : 'us');
return {
id,
type,
refID,
key: match[3] || null,
uri: `apple_music:${type}:${id ? `${refID}i` : refID}${id || ''}`,
url: `https://music.apple.com/${storefront}/${collection_type}/${refID}${id ? `?i=${id}` : ''}`,
uri: `apple_music:${type}:${id}`,
url: `https://music.apple.com/${storefront}/${scope}/${id}`,
storefront,
collection_type,
};
@ -117,7 +114,7 @@ export default class AppleMusic {
name: trackInfo.attributes.name,
artists: [trackInfo.attributes.artistName],
album: albumInfo.name,
album_uri: `apple_music:album:${albumInfo.id || this.parseURI(trackInfo.attributes.url).refID}`,
album_uri: `apple_music:album:${albumInfo.id}`,
album_type: albumInfo.type,
images: trackInfo.attributes.artwork,
duration: trackInfo.attributes.durationInMillis,
@ -206,7 +203,7 @@ export default class AppleMusic {
const parsed = this.parseURI(_uri, store);
if (!parsed) return [];
parsed.value = this.#store.cache.get(parsed.uri);
return [[parsed.id || parsed.refID, parsed]];
return [[parsed.id, parsed]];
});
const packs = uris.filter(([, {value}]) => !value).map(([, parsed]) => parsed);
uris = Object.fromEntries(uris);
@ -246,7 +243,7 @@ export default class AppleMusic {
return this.processData(uris, 300, store, async (items, storefront) => {
const {data: tracks} = await this.#store.core.songs.get(`?ids=${items.map(item => item.id).join(',')}`, {storefront});
await this.getAlbum(
items.map(item => `apple_music:album:${item.refID}`),
tracks.flatMap(item => item.relationships.albums.data.map(item => `apple_music:album:${item.id}`)),
storefront,
);
return Promise.mapSeries(tracks, async track => {
@ -260,9 +257,14 @@ export default class AppleMusic {
throw err;
// this.#store.core.songs.get(`${track.id}${nextUrl.split(track.href)[1]}`, {storefront});
});
if (track.albums.length > 1) {
let err = new Error('Unimplemented: track with multiple albums');
[err.trackId, err.trackHref] = [track.id, track.href];
throw err;
}
return this.wrapTrackMeta(
track,
await this.getAlbum(`apple_music:album:${this.parseURI(track.attributes.url).refID}`, storefront),
await this.getAlbum(`apple_music:album:${track.relationships.albums.data[0].id}`, storefront),
);
});
});
@ -271,7 +273,7 @@ export default class AppleMusic {
async getAlbum(uris, store) {
return this.processData(uris, 100, store, async (items, storefront) =>
Promise.mapSeries(
(await this.#store.core.albums.get(`?ids=${items.map(item => item.refID).join(',')}`, {storefront})).data,
(await this.#store.core.albums.get(`?ids=${items.map(item => item.id).join(',')}`, {storefront})).data,
async album => {
album.tracks = await this.depaginate(album.relationships.tracks, nextUrl => {
let err = new Error('Unimplemented: album tracks pagination');
@ -295,7 +297,7 @@ export default class AppleMusic {
async getArtist(uris, store) {
return this.processData(uris, 25, store, async (items, storefront) =>
Promise.mapSeries(
(await this.#store.core.artists.get(`?ids=${items.map(item => item.refID).join(',')}`, {storefront})).data,
(await this.#store.core.artists.get(`?ids=${items.map(item => item.id).join(',')}`, {storefront})).data,
async artist => {
artist.albums = await this.depaginate(artist.relationships.albums, nextUrl =>
this.#store.core.artists.get(`${artist.id}${nextUrl.split(artist.href)[1]}`, {storefront}),
@ -309,7 +311,7 @@ export default class AppleMusic {
async getPlaylist(uris, store) {
return this.processData(uris, 25, store, async (items, storefront) =>
Promise.mapSeries(
(await this.#store.core.playlists.get(`?ids=${items.map(item => item.refID).join(',')}`, {storefront})).data,
(await this.#store.core.playlists.get(`?ids=${items.map(item => item.id).join(',')}`, {storefront})).data,
async playlist => {
playlist.tracks = await this.depaginate(playlist.relationships.tracks, nextUrl =>
this.#store.core.playlists.get(`${playlist.id}${nextUrl.split(playlist.href)[1]}`, {storefront}),

83
test/urify.js Normal file
View File

@ -0,0 +1,83 @@
import FreyrCore from '../src/freyr.js';
let corpus = [
{
url: 'https://open.spotify.com/track/127QTOFJsJQp5LbJbu3A1y',
uri: 'spotify:track:127QTOFJsJQp5LbJbu3A1y',
},
{
url: 'https://open.spotify.com/album/623PL2MBg50Br5dLXC9E9e',
uri: 'spotify:album:623PL2MBg50Br5dLXC9E9e',
},
{
url: 'https://open.spotify.com/artist/6M2wZ9GZgrQXHCFfjv46we',
uri: 'spotify:artist:6M2wZ9GZgrQXHCFfjv46we',
},
{
url: 'https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M',
uri: 'spotify:playlist:37i9dQZF1DXcBWIGoYBM5M',
},
{
url: 'https://music.apple.com/us/album/say-so-feat-nicki-minaj/1510821672?i=1510821685',
uri: 'apple_music:track:1510821685',
},
{
url: 'https://music.apple.com/us/song/1510821685',
uri: 'apple_music:track:1510821685',
},
{
url: 'https://music.apple.com/us/album/birds-of-prey-the-album/1493581254',
uri: 'apple_music:album:1493581254',
},
{
url: 'https://music.apple.com/us/artist/412778295',
uri: 'apple_music:artist:412778295',
},
{
url: 'https://music.apple.com/us/playlist/todays-hits/pl.f4d106fed2bd41149aaacabb233eb5eb',
uri: 'apple_music:playlist:pl.f4d106fed2bd41149aaacabb233eb5eb',
},
{
url: 'https://www.deezer.com/en/track/642674232',
uri: 'deezer:track:642674232',
},
{
url: 'https://www.deezer.com/en/album/99687992',
uri: 'deezer:album:99687992',
},
{
url: 'https://www.deezer.com/en/artist/5340439',
uri: 'deezer:artist:5340439',
},
{
url: 'https://www.deezer.com/en/playlist/1963962142',
uri: 'deezer:playlist:1963962142',
},
];
function main() {
for (let item of corpus) {
for (let key in item) {
let parsed = FreyrCore.parseURI(item[key]);
if (parsed) {
console.log(`⏩┬[ \x1b[36m${item[key]}\x1b[39m ]`);
if (parsed.uri === item.uri) {
console.log(` ├ ✅ asURI -> \x1b[36m${parsed.uri}\x1b[39m`);
} else {
console.log(` ├ ❌ asURI -> \x1b[36m${parsed.uri}\x1b[39m (expected \x1b[33m${item.uri}\x1b[39m)`);
}
if (parsed.url === item.url) {
console.log(` └ ✅ asURL -> \x1b[36m${parsed.url}\x1b[39m`);
} else {
console.log(` └ ❌ asURL -> \x1b[36m${parsed.url}\x1b[39m (expected \x1b[33m${item.url}\x1b[39m)`);
}
} else {
console.log(`❌─[ \x1b[36m${item[key]}\x1b[39m ]`);
}
}
console.log();
}
}
main();