spotify-url-info
Advanced tools
Comparing version 3.0.0 to 3.0.1
@@ -5,3 +5,3 @@ { | ||
"homepage": "https://github.com/microlinkhq/spotify-url-info", | ||
"version": "3.0.0", | ||
"version": "3.0.1", | ||
"main": "src/index.js", | ||
@@ -16,18 +16,2 @@ "author": { | ||
"email": "josefrancisco.verdu@gmail.com" | ||
}, | ||
{ | ||
"name": "DaliborTrampota", | ||
"email": "dalibor.trampota@gmail.com" | ||
}, | ||
{ | ||
"name": "crxts", | ||
"email": "49580728+crxts@users.noreply.github.com" | ||
}, | ||
{ | ||
"name": "kaaax0815", | ||
"email": "999999bst@gmail.com" | ||
}, | ||
{ | ||
"name": "KeepSOBP", | ||
"email": "keepsobp@naver.com" | ||
} | ||
@@ -61,5 +45,5 @@ ], | ||
"conventional-github-releaser": "latest", | ||
"cross-fetch": "latest", | ||
"finepack": "latest", | ||
"git-authors-cli": "latest", | ||
"isomorphic-unfetch": "latest", | ||
"nano-staged": "latest", | ||
@@ -66,0 +50,0 @@ "npm-check-updates": "latest", |
237
src/index.js
'use strict' | ||
const spotifyURI = require('spotify-uri') | ||
const { parse } = require('himalaya') | ||
const SUPPORTED_TYPES = ['album', 'artist', 'episode', 'playlist', 'track'] | ||
const TYPE = { | ||
ALBUM: 'album', | ||
ARTIST: 'artist', | ||
EPISODE: 'episode', | ||
PLAYLIST: 'playlist', | ||
TRACK: 'track' | ||
} | ||
const createGetData = fetch => (url, opts) => { | ||
let parsedURL = {} | ||
const SUPPORTED_TYPES = Object.values(TYPE) | ||
try { | ||
parsedURL = spotifyURI.parse(url) | ||
} catch (error) { | ||
return Promise.reject(new TypeError(`Couldn't parse '${url}' as valid URL`)) | ||
const createGetData = fetch => async (url, opts) => { | ||
const parsedUrl = getParsedUrl(url) | ||
const embedURL = spotifyURI.formatEmbedURL(parsedUrl) | ||
const response = await fetch(embedURL, opts) | ||
const text = await response.text() | ||
const embed = parse(text) | ||
const scripts = embed | ||
.filter(e => e.tagName === 'html')[0] | ||
.children.filter(e => e.tagName === 'body')[0] | ||
.children.filter(e => e.tagName === 'script') | ||
const resourceScript = scripts.filter( | ||
e => e.attributes.findIndex(a => a.value === 'resource') !== -1 | ||
) | ||
const hydrateScript = scripts.filter( | ||
e => e.children[0] && /%22data%22%|"data":/.test(e.children[0].content) | ||
) | ||
if (resourceScript.length > 0) { | ||
// found data in the older embed style | ||
return JSON.parse(decodeURIComponent(resourceScript[0].children[0].content)) | ||
} | ||
if (!parsedURL.type) { | ||
return Promise.reject( | ||
new TypeError(`Failed to parse '${url}' as Spotify URL`) | ||
if (hydrateScript.length > 0) { | ||
// found hydration data | ||
// parsing via looking for { to be a little bit resistant to code changes | ||
const scriptContent = hydrateScript[0].children[0].content.includes( | ||
'%22data%22%' | ||
) | ||
? decodeURIComponent(hydrateScript[0].children[0].content) | ||
: hydrateScript[0].children[0].content | ||
return normalizeData( | ||
JSON.parse( | ||
'{' + | ||
scriptContent | ||
.split('{') | ||
.slice(1) | ||
.join('{') | ||
.trim() | ||
) | ||
) | ||
} | ||
const embedURL = spotifyURI.formatEmbedURL(parsedURL) | ||
throw new Error( | ||
"Couldn't find any data in embed page that we know how to parse" | ||
) | ||
} | ||
return fetch(embedURL, opts) | ||
.then(res => res.text()) | ||
.then(parse) | ||
.then(embed => { | ||
const scripts = embed | ||
.filter(e => e.tagName === 'html')[0] | ||
.children.filter(e => e.tagName === 'body')[0] | ||
.children.filter(e => e.tagName === 'script') | ||
const resourceScript = scripts.filter( | ||
e => e.attributes.findIndex(a => a.value === 'resource') !== -1 | ||
) | ||
const hydrateScript = scripts.filter( | ||
e => e.children[0] && /%22data%22%|"data":/.test(e.children[0].content) | ||
) | ||
function getParsedUrl (url) { | ||
try { | ||
const parsedURL = spotifyURI.parse(url) | ||
if (!parsedURL.type) throw new TypeError() | ||
return spotifyURI.formatEmbedURL(parsedURL) | ||
} catch (_) { | ||
throw new TypeError(`Couldn't parse '${url}' as valid URL`) | ||
} | ||
} | ||
if (resourceScript.length > 0) { | ||
// found data in the older embed style | ||
return JSON.parse( | ||
decodeURIComponent(resourceScript[0].children[0].content) | ||
) | ||
} else if (hydrateScript.length > 0) { | ||
// found hydration data | ||
// parsing via looking for { to be a little bit resistant to code changes | ||
const scriptContent = hydrateScript[0].children[0].content.includes( | ||
'%22data%22%' | ||
) | ||
? decodeURIComponent(hydrateScript[0].children[0].content) | ||
: hydrateScript[0].children[0].content | ||
const data = JSON.parse( | ||
'{' + | ||
scriptContent | ||
.split('{') | ||
.slice(1) | ||
.join('{') | ||
.trim() | ||
).data | ||
return data.entity ? data.entity : data | ||
} else { | ||
return Promise.reject( | ||
new Error( | ||
"Couldn't find any data in embed page that we know how to parse" | ||
) | ||
) | ||
} | ||
}) | ||
.then(sanityCheck) | ||
function getImages (data) { | ||
switch (data.type) { | ||
case TYPE.TRACK: | ||
return data.album.images | ||
case TYPE.EPISODE: | ||
return data.coverArt.sources | ||
default: | ||
return data.images | ||
} | ||
} | ||
function toPreview (data) { | ||
const track = getFirstTrack(data) | ||
const images = data.type === 'track' ? data.album.images : data.images | ||
const date = data.album ? data.album.release_date : data.release_date | ||
function getDate (data) { | ||
switch (data.type) { | ||
case TYPE.TRACK: | ||
return data.album.release_date | ||
case TYPE.EPISODE: | ||
return data.releaseDate.isoString | ||
default: | ||
return data.release_date | ||
} | ||
} | ||
const artist = track.show | ||
function getArtistTrack (track) { | ||
return track.show | ||
? track.show.publisher | ||
@@ -86,5 +107,18 @@ : [] | ||
.join(' & ') | ||
} | ||
return Promise.resolve({ | ||
date, | ||
function getLink (data) { | ||
switch (data.type) { | ||
case TYPE.EPISODE: | ||
return data.sharingInfo.shareUrl | ||
default: | ||
return data.external_urls.spotify | ||
} | ||
} | ||
function getPreview (data) { | ||
const track = getFirstTrack(data) | ||
return { | ||
date: getDate(data), | ||
title: data.name, | ||
@@ -94,25 +128,20 @@ type: data.type, | ||
description: data.description || undefined, | ||
artist: artist || undefined, | ||
image: images.reduce((a, b) => (a.width > b.width ? a : b)).url, | ||
artist: getArtistTrack(track), | ||
image: getImages(data).reduce((a, b) => (a.width > b.width ? a : b)).url, | ||
audio: track.audio_preview_url || track.preview_url, | ||
link: data.external_urls.spotify, | ||
link: getLink(data), | ||
embed: `https://embed.spotify.com/?uri=${data.uri}` | ||
}) | ||
} | ||
} | ||
function getTracks (data) { | ||
if (!data.tracks) { | ||
// Is a track or a podcast episode | ||
return Promise.resolve([data]) | ||
} else if (data.tracks.items) { | ||
if (data.tracks.items[0].track) { | ||
// Is a playlist | ||
return Promise.resolve(data.tracks.items.map(t => t.track)) | ||
} else { | ||
// Is an album | ||
return Promise.resolve(data.tracks.items) | ||
} | ||
} else { | ||
// Is an artist | ||
return Promise.resolve(data.tracks) | ||
switch (data.type) { | ||
case TYPE.PLAYLIST: | ||
return data.tracks.items.map(({ track }) => track) | ||
case TYPE.ARTIST: | ||
return data.tracks | ||
case TYPE.ALBUM: | ||
return data.tracks.items | ||
default: | ||
return [data] | ||
} | ||
@@ -123,36 +152,40 @@ } | ||
switch (data.type) { | ||
case 'track': | ||
case TYPE.TRACK: | ||
return data | ||
case 'playlist': | ||
case TYPE.PLAYLIST: | ||
return data.tracks.items[0].track | ||
case 'album': | ||
case TYPE.ALBUM: | ||
return data.tracks.items[0] | ||
case 'artist': | ||
case TYPE.ARTIST: | ||
return data.tracks[0] | ||
case 'episode': | ||
case TYPE.EPISODE: { | ||
const { podcast, audioPreview } = data | ||
return { | ||
artists: data.show.publisher.split(' and ').map(name => ({ name })), | ||
name: data.show.name, | ||
preview_url: data.audio_preview_url | ||
artists: podcast.publisher.name.split(' and ').map(name => ({ name })), | ||
name: podcast.name, | ||
preview_url: audioPreview.url | ||
} | ||
default: | ||
return data | ||
} | ||
} | ||
} | ||
function sanityCheck (data) { | ||
function normalizeData ({ data }) { | ||
data = data.entity ? data.entity : data | ||
if (data.episode) { | ||
data = data.episode | ||
data.type = TYPE.EPISODE | ||
} | ||
if (!data || !data.type || !data.name) { | ||
return Promise.reject( | ||
new Error("Data doesn't seem to be of the right shape to parse") | ||
) | ||
throw new Error("Data doesn't seem to be of the right shape to parse") | ||
} | ||
if (!SUPPORTED_TYPES.includes(data.type)) { | ||
return Promise.reject( | ||
new Error( | ||
`Not an ${SUPPORTED_TYPES.join(', ')}. Only these types can be parsed` | ||
) | ||
throw new Error( | ||
`Not an ${SUPPORTED_TYPES.join(', ')}. Only these types can be parsed` | ||
) | ||
} | ||
return Promise.resolve(data) | ||
return data | ||
} | ||
@@ -164,5 +197,5 @@ | ||
getData, | ||
getPreview: (url, opts) => getData(url, opts).then(toPreview), | ||
getPreview: (url, opts) => getData(url, opts).then(getPreview), | ||
getTracks: (url, opts) => getData(url, opts).then(getTracks) | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
213
12596
83