diff --git a/lib/services/itunes/index.js b/lib/services/itunes/index.js index c70a2f7..2c699cf 100644 --- a/lib/services/itunes/index.js +++ b/lib/services/itunes/index.js @@ -1,28 +1,30 @@ -import { parse } from 'url'; -import querystring from 'querystring'; -import request from 'superagent'; -import urlMatch from './url.js'; +import { parse } from "url"; +import querystring from "querystring"; +import request from "superagent"; +import urlMatch from "./url.js"; -const apiRoot = 'https://itunes.apple.com'; +const apiRoot = "https://itunes.apple.com"; export async function parseUrl(url) { const parsed = parse(url); - const matches = parsed.path.match(/[/]?([/]?[a-z]{2}?)?[/]+album[/]+([^/]+)[/]+([^?]+)/); + const matches = parsed.path.match( + /[/]?([/]?[a-z]{2}?)?[/]+(song|album)[/]+([^/]+)[/]+([^?]+)/ + ); const query = querystring.parse(parsed.query); - let itunesId = matches[3]; + let itunesId = matches[4]; if (matches) { - let type = 'album'; + let type = "album"; if (matches[3].match(/^id/)) { itunesId = matches[3].substr(2); if (query.i) { - type = 'track'; + type = "track"; itunesId = query.i; } } - return await lookupId(itunesId, type, matches[1] || 'us'); + return await lookupId(itunesId, type, matches[1] || "us"); } throw new Error(); } @@ -44,61 +46,74 @@ export async function lookupId(possibleId, type, countrycode) { const response = await request.get(apiRoot + path); let result = JSON.parse(response.text); - if (!result.results || result.resultCount === 0 || !result.results[0].collectionId) { + if ( + !result.results || + result.resultCount === 0 || + !result.results[0].collectionId + ) { throw new Error(); } else { result = result.results[0]; const item = { - service: 'itunes', + service: "itunes", type, id: cc + id, name: result.trackName ? result.trackName : result.collectionName, streamUrl: null, purchaseUrl: result.collectionViewUrl, artwork: { - small: `${result.artworkUrl100.replace('100x100', '200x200').replace('.mzstatic.com', '.mzstatic.com').replace('http://', 'https://')}`, - large: `${result.artworkUrl100.replace('100x100', '600x600').replace('.mzstatic.com', '.mzstatic.com').replace('http://', 'https://')}`, + small: `${result.artworkUrl100 + .replace("100x100", "200x200") + .replace(".mzstatic.com", ".mzstatic.com") + .replace("http://", "https://")}`, + large: `${result.artworkUrl100 + .replace("100x100", "600x600") + .replace(".mzstatic.com", ".mzstatic.com") + .replace("http://", "https://")}` }, artist: { - name: result.artistName, - }, + name: result.artistName + } }; - if (type === 'track') { + if (type === "track") { item.album = { - name: result.collectionName, + name: result.collectionName }; } return item; } - } catch(e) { - const error = new Error('Not Found'); + } catch (e) { + const error = new Error("Not Found"); error.status = 404; return Promise.reject(error); } } export async function search(data) { - const markets = ['us', 'gb', 'jp', 'br', 'de', 'es']; + const markets = ["us", "gb", "jp", "br", "de", "es"]; let query; let album; let entity; const type = data.type; - if (type === 'album') { + if (type === "album") { query = `${data.artist.name} ${data.name}`; album = data.name; - entity = 'album'; - } else if (type === 'track') { + entity = "album"; + } else if (type === "track") { query = `${data.artist.name} ${data.albumName} ${data.name}`; album = data.albumName; - entity = 'musicTrack'; + entity = "musicTrack"; } - for (const market of markets) { // eslint-disable-line - const path = `/${market}/search?term=${encodeURIComponent(query)}&media=music&entity=${entity}`; + for (const market of markets) { + // eslint-disable-line + const path = `/${market}/search?term=${encodeURIComponent( + query + )}&media=music&entity=${entity}`; const response = await request.get(apiRoot + path); let result = JSON.parse(response.text); @@ -106,9 +121,9 @@ export async function search(data) { const matches = album.match(/^[^([]+/); if (matches && matches[0] && matches[0] !== album) { const cleanedData = JSON.parse(JSON.stringify(data)); - if (type === 'album') { + if (type === "album") { cleanedData.name = matches[0].trim(); - } else if (type === 'track') { + } else if (type === "track") { cleanedData.albumName = matches[0].trim(); } return await search(cleanedData); @@ -117,31 +132,37 @@ export async function search(data) { result = result.results[0]; const item = { - service: 'itunes', + service: "itunes", type, id: `us${result.collectionId}`, name: result.trackName ? result.trackName : result.collectionName, streamUrl: result.collectionViewUrl, purchaseUrl: result.collectionViewUrl, artwork: { - small: `${result.artworkUrl100.replace('100x100', '200x200').replace('.mzstatic.com', '.mzstatic.com').replace('http://', 'https://')}`, - large: `${result.artworkUrl100.replace('100x100', '600x600').replace('.mzstatic.com', '.mzstatic.com').replace('http://', 'https://')}`, + small: `${result.artworkUrl100 + .replace("100x100", "200x200") + .replace(".mzstatic.com", ".mzstatic.com") + .replace("http://", "https://")}`, + large: `${result.artworkUrl100 + .replace("100x100", "600x600") + .replace(".mzstatic.com", ".mzstatic.com") + .replace("http://", "https://")}` }, artist: { - name: result.artistName, - }, + name: result.artistName + } }; - if (type === 'track') { + if (type === "track") { item.album = { - name: result.collectionName, + name: result.collectionName }; } return item; } } - return { service: 'itunes' }; + return { service: "itunes" }; } -export const id = 'itunes'; +export const id = "itunes"; export const match = urlMatch; diff --git a/lib/services/itunes/url.js b/lib/services/itunes/url.js index 6392e74..a0b4245 100644 --- a/lib/services/itunes/url.js +++ b/lib/services/itunes/url.js @@ -1,13 +1,18 @@ -import { parse } from 'url'; +import { parse } from "url"; export default function match(url) { const parsed = parse(url); - if (!parsed.host.match(/itunes.apple\.com$/)) { + if ( + !parsed.host.match(/itunes\.apple\.com$/) && + !parsed.host.match(/music\.apple\.com$/) + ) { return false; } - const matches = parsed.path.match(/[/]?([/]?[a-z]{2}?)?[/]+album[/]+([^/]+)[/]+([^?]+)/); + const matches = parsed.path.match( + /[/]?([/]?[a-z]{2}?)?[/]+(song|album)[/]+([^/]+)[/]+([^?]+)/ + ); - return !!matches[3]; + return !!matches[4]; } diff --git a/lib/services/ytmusic/index.js b/lib/services/ytmusic/index.js index 920ffec..96c17c7 100644 --- a/lib/services/ytmusic/index.js +++ b/lib/services/ytmusic/index.js @@ -1,73 +1,133 @@ -import urlMatch from './url.js'; -import querystring from 'querystring'; -import request from 'superagent'; -import { parse } from 'url'; -import debuglog from 'debug'; +import urlMatch from "./url.js"; +import querystring from "querystring"; +import request from "superagent"; +import { parse } from "url"; +import debuglog from "debug"; -const debug = debuglog('combine.fm:ytmusic'); +const debug = debuglog("combine.fm:ytmusic"); + +const standard_body = { + context: { + capabilities: {}, + client: { + clientName: "WEB_REMIX", + clientVersion: "1.20250514.03.00", + experimentIds: [], + experimentsToken: "", + gl: "DE", + hl: "en", + locationInfo: { + locationPermissionAuthorizationStatus: + "LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED" + }, + musicAppInfo: { + musicActivityMasterSwitch: "MUSIC_ACTIVITY_MASTER_SWITCH_INDETERMINATE", + musicLocationMasterSwitch: "MUSIC_LOCATION_MASTER_SWITCH_INDETERMINATE", + pwaInstallabilityStatus: "PWA_INSTALLABILITY_STATUS_UNKNOWN" + }, + utcOffsetMinutes: 60 + }, + request: { + internalExperimentFlags: [ + { + key: "force_music_enable_outertube_tastebuilder_browse", + value: "true" + }, + { + key: "force_music_enable_outertube_playlist_detail_browse", + value: "true" + }, + { + key: "force_music_enable_outertube_search_suggestions", + value: "true" + } + ], + sessionIndex: {} + }, + user: { enableSafetyMode: false } + } +}; + +// {"context":{"client":{"hl":"en","gl":"IE","remoteHost":"109.255.114.183","deviceMake":"","deviceModel":"","visitorData":"Cgs1WHNrQ2ZJSlZQYyju17HBBjInCgJJRRIhEh0SGwsMDg8QERITFBUWFxgZGhscHR4fICEiIyQlJiBG","userAgent":"Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0,gzip(gfe)","clientName":"WEB_REMIX","clientVersion":"1.20250514.03.00","osName":"X11","osVersion":"","originalUrl":"https://music.youtube.com/youtube/v1/search","screenPixelDensity":2,"platform":"DESKTOP","clientFormFactor":"UNKNOWN_FORM_FACTOR","configInfo":{"appInstallData":"CO7XscEGEOGCgBMQpZ3PHBDtoM8cEP7z_xIQmvTOHBCJsM4cEIeszhwQ2vfOHBDM364FEL2azxwQgc3OHBCLgoATEN68zhwQyfevBRDn484cEJmNsQUQvoqwBRDvnc8cEO79zhwQ6-j-EhCd0LAFEN-4zhwQ6ZvPHBD8ss4cEODczhwQnJvPHBD-ns8cELvZzhwQt-r-EhC0jIATEOK4sAUQgoS4IhDJ5rAFEPDizhwQiIewBRC9tq4FEJT-sAUQmZixBRC52c4cEIjjrwUQ9quwBRC9mbAFELCJzxwQ9v7_EhDwnLAFENeczxwQ0-GvBRDMic8cEOTn_xIQ4Z7PHBC45M4cEODg_xIQoaHPHBCb-M4cKixDQU1TR3hVUW9MMndETkhrQnBTQ0V0WFM2Z3Y1N0FQSjNBV2hwQVFkQnc9PQ%3D%3D","coldConfigData":"CO7XscEGGjJBT2pGb3gzcE9TeHpiVUxCMkdoT0xCZHI0eThDNVRLSTN4OXhLVXR0V2h0RGJaZmZnZyIyQU9qRm94MXFoUkxFb2JKVzlJd2dWZm8wVWJJbHNrTTllQUZyVF81UkJBZHZSSTRjTmc%3D","coldHashData":"CO7XscEGEhM4MzcyMjg4Nzg1MDY2MDg0NzkyGO7XscEGMjJBT2pGb3gzcE9TeHpiVUxCMkdoT0xCZHI0eThDNVRLSTN4OXhLVXR0V2h0RGJaZmZnZzoyQU9qRm94MXFoUkxFb2JKVzlJd2dWZm8wVWJJbHNrTTllQUZyVF81UkJBZHZSSTRjTmc%3D","hotHashData":"CO7XscEGEhQxMTE4MDExNDAyNDI2OTEwMTQxMxju17HBBjIyQU9qRm94M3BPU3h6YlVMQjJHaE9MQmRyNHk4QzVUS0kzeDl4S1V0dFdodERiWmZmZ2c6MkFPakZveDFxaFJMRW9iSlc5SXdnVmZvMFViSWxza005ZUFGclRfNVJCQWR2Ukk0Y05n"},"screenDensityFloat":2,"userInterfaceTheme":"USER_INTERFACE_THEME_DARK","timeZone":"Europe/Dublin","browserName":"Firefox","browserVersion":"138.0","acceptHeader":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","deviceExperimentId":"ChxOelV3TmpRNU16TTFNRGMxTXpjNU9EZzBPQT09EO7XscEGGO7XscEG","rolloutToken":"CODP7vvb9ci_dRC-iJ6MorWMAxi-vbDI7q-NAw%3D%3D","screenWidthPoints":1440,"screenHeightPoints":434,"utcOffsetMinutes":60,"musicAppInfo":{"pwaInstallabilityStatus":"PWA_INSTALLABILITY_STATUS_UNKNOWN","webDisplayMode":"WEB_DISPLAY_MODE_BROWSER","storeDigitalGoodsApiSupportStatus":{"playStoreDigitalGoodsApiSupportStatus":"DIGITAL_GOODS_API_SUPPORT_STATUS_UNSUPPORTED"}}},"user":{"lockedSafetyMode":false},"request":{"useSsl":true,"internalExperimentFlags":[],"consistencyTokenJars":[]},"adSignalsInfo":{"params":[{"key":"dt","value":"1747741679421"},{"key":"flash","value":"0"},{"key":"frm","value":"0"},{"key":"u_tz","value":"60"},{"key":"u_his","value":"2"},{"key":"u_h","value":"960"},{"key":"u_w","value":"1440"},{"key":"u_ah","value":"960"},{"key":"u_aw","value":"1440"},{"key":"u_cd","value":"24"},{"key":"bc","value":"31"},{"key":"bih","value":"434"},{"key":"biw","value":"1440"},{"key":"brdim","value":"0,0,0,0,1440,0,1440,928,1440,434"},{"key":"vis","value":"1"},{"key":"wgl","value":"true"},{"key":"ca_type","value":"image"}]}},"query":"banger","suggestStats":{"validationStatus":"VALID","parameterValidationStatus":"VALID_PARAMETERS","clientName":"youtube-music","searchMethod":"ENTER_KEY","inputMethods":["KEYBOARD"],"originalQuery":"banger","availableSuggestions":[{"index":0,"type":0},{"index":1,"type":0},{"index":2,"type":0},{"index":3,"type":0},{"index":4,"type":0},{"index":5,"type":0},{"index":6,"type":46},{"index":7,"type":46},{"index":8,"type":46}],"zeroPrefixEnabled":true,"firstEditTimeMsec":13046,"lastEditTimeMsec":14576}} -const standard_body = {'context': {'capabilities': {}, 'client': {'clientName': 'WEB_REMIX', 'clientVersion': '0.1', 'experimentIds': [], 'experimentsToken': '', 'gl': 'DE', 'hl': 'en', 'locationInfo': {'locationPermissionAuthorizationStatus': 'LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED'}, 'musicAppInfo': {'musicActivityMasterSwitch': 'MUSIC_ACTIVITY_MASTER_SWITCH_INDETERMINATE', 'musicLocationMasterSwitch': 'MUSIC_LOCATION_MASTER_SWITCH_INDETERMINATE', 'pwaInstallabilityStatus': 'PWA_INSTALLABILITY_STATUS_UNKNOWN'}, 'utcOffsetMinutes': 60}, 'request': {'internalExperimentFlags': [{'key': 'force_music_enable_outertube_tastebuilder_browse', 'value': 'true'}, {'key': 'force_music_enable_outertube_playlist_detail_browse', 'value': 'true'}, {'key': 'force_music_enable_outertube_search_suggestions', 'value': 'true'}], 'sessionIndex': {}}, 'user': {'enableSafetyMode': false}}} const standard_headers = { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0", - "Accept": "*/*", + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0", + Accept: "*/*", "Accept-Language": "en-US,en;q=0.5", "Content-Type": "application/json", "X-Goog-AuthUser": "0", - "origin": "https://music.youtube.com", + origin: "https://music.youtube.com", "X-Goog-Visitor-Id": "CgtWaTB2WWRDeEFUYyjhv-X8BQ%3D%3D" -} -const standard_params = { alt: "json", key: "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30"} // INNERTUBE_API_KEY from music.youtube.com +}; +const standard_params = { + alt: "json", + key: "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30" +}; // INNERTUBE_API_KEY from music.youtube.com -const base_filter = "Eg-KAQwIA" -const albums_filter = "BAAGAEgACgA" -const tracks_filter = "RAAGAAgACgA" +const base_filter = "Eg-KAQwIA"; +const albums_filter = "BAAGAEgACgA"; +const tracks_filter = "RAAGAAgACgA"; // If you make a typo, ytmusic searches for a correction. With this filter it will look for the exact match // since we don't let users type, no sense in letting it autocorrect -const exact_search_filter = "MABqChAEEAMQCRAFEAo%3D" +const exact_search_filter = "MABqChAEEAMQCRAFEAo%3D"; // The logic here comes from https://github.com/sigma67/ytmusicapi // If something doesn't work, looking up back there might be a good idea. export async function search(data, original = {}) { let query; - const various = data.artist.name === 'Various Artists' || data.artist.name === 'Various'; + const various = + data.artist.name === "Various Artists" || data.artist.name === "Various"; if (various) { data.artist.name = ""; } if (data.type == "track") { - query = [data.name, data.artist.name, data.albumName] + query = [data.name, data.artist.name, data.albumName]; } else if (data.type == "album") { - query = [data.name, data.artist.name] + query = [data.name, data.artist.name]; } else { throw new Error(); } // Add "" to try and make the search better, works for stuff like "The beatles" to reduce noise - query = query.filter(String).map((entry) => '"' + entry + '"').join(" ") + query = query + .filter(String) + .map(entry => '"' + entry + '"') + .join(" "); - let params = base_filter + (data.type == "track" ? tracks_filter : albums_filter) + exact_search_filter - let request_body = {query, params, ...standard_body } + let params = + base_filter + + (data.type == "track" ? tracks_filter : albums_filter) + + exact_search_filter; + let request_body = { query, params, ...standard_body }; - const { body } = await request.post("https://music.youtube.com/youtubei/v1/search") - .set(standard_headers) - .query(standard_params) - .send(request_body) + try { + const { body } = await request + .post("https://music.youtube.com/youtubei/v1/search") + .set(standard_headers) + .query(standard_params) + .send(request_body); + } catch (err) { + debug(err); + } // no results if (body.contents === undefined) { - debug("Empty body, no results") - return { service: 'ytmusic' }; + debug("Empty body, no results"); + return { service: "ytmusic" }; } let results; if (body.contents.tabbedSearchResultsRenderer !== undefined) { - results = body.contents.tabbedSearchResultsRenderer.tabs[0].tabRenderer.content + results = + body.contents.tabbedSearchResultsRenderer.tabs[0].tabRenderer.content; } else { - results = body.contents.sectionListRenderer.contents + results = body.contents.sectionListRenderer.contents; } // no results if (results.length == 1 && results.itemSectionRenderer !== undefined) { - debug("Only itemSectionRenderer, no results") - return { service: 'ytmusic' }; + debug("Only itemSectionRenderer, no results"); + return { service: "ytmusic" }; } for (const result of results) { @@ -75,130 +135,160 @@ export async function search(data, original = {}) { continue; } - const matches = parse_result_content(result.musicShelfRenderer.contents, data.type) + const matches = parse_result_content( + result.musicShelfRenderer.contents, + data.type + ); // This could probably be done without extra lookups, but it would involve parsing deeply the response. // If there's some kind of rate limit on ytmusic's side, this is a good play to start refactoring for (const match of matches) { - const possibleMatch = await lookupId(match, data.type) + const possibleMatch = await lookupId(match, data.type); const nameMatch = possibleMatch.name == data.name; - const artistMatch = data.artist.name == "" ? possibleMatch.artist.name === 'Various Artists' : data.artist.name == possibleMatch.artist.name; + const artistMatch = + data.artist.name == "" + ? possibleMatch.artist.name === "Various Artists" + : data.artist.name == possibleMatch.artist.name; if (nameMatch && artistMatch) { - return possibleMatch + return possibleMatch; } } } - debug("Finished looking up, no results") - return { service: 'ytmusic' }; + debug("Finished looking up, no results"); + return { service: "ytmusic" }; } function parse_result_content(contents, type) { - let matches = [] + let matches = []; for (const result of contents) { const data = result.musicResponsiveListItemRenderer; - const informed_type = data.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text + const informed_type = + data.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[0] + .text; if (["Video", "Playlist"].includes(informed_type)) { continue; } let matchId; if (type == "track") { - matchId = data.overlay?.musicItemThumbnailOverlayRenderer.content.musicPlayButtonRenderer.playNavigationEndpoint.watchEndpoint?.videoId + matchId = + data.overlay?.musicItemThumbnailOverlayRenderer.content + .musicPlayButtonRenderer.playNavigationEndpoint.watchEndpoint + ?.videoId; } else if (type == "album") { - matchId = data.navigationEndpoint?.browseEndpoint.browseId + matchId = data.navigationEndpoint?.browseEndpoint.browseId; } - if(matchId) { - matches.push(matchId) + if (matchId) { + matches.push(matchId); } } - return matches + return matches; } async function lookupTrack(id) { - let request_body = {'video_id': id, ...standard_body } + let request_body = { video_id: id, ...standard_body }; - const { body } = await request.post("https://music.youtube.com/youtubei/v1/player") + const { body } = await request + .post("https://music.youtube.com/youtubei/v1/player") .set(standard_headers) .query(standard_params) - .send(request_body) - let song_meta = body.videoDetails + .send(request_body); + let song_meta = body.videoDetails; - let description = body.microformat.microformatDataRenderer.description.split(' · ') - let possible_album_name = description[description.length - 1].split("℗")[0] + let description = body.microformat.microformatDataRenderer.description.split( + " · " + ); + let possible_album_name = description[description.length - 1].split("℗")[0]; if (!description[description.length - 1].includes("℗")) { possible_album_name = ""; } - let tags = body.microformat.microformatDataRenderer.tags - let album_name = "" + let tags = body.microformat.microformatDataRenderer.tags; + let album_name = ""; for (const tag of tags) { - if(possible_album_name.includes(tag)){ + if (possible_album_name.includes(tag)) { album_name = tag; } } - let artists = song_meta.author - artists = artists.replace(" - Topic", "") + let artists = song_meta.author; + artists = artists.replace(" - Topic", ""); const artwork = { small: song_meta.thumbnail.thumbnails[0].url, - large: song_meta.thumbnail.thumbnails[song_meta.thumbnail.thumbnails.length-1].url, + large: + song_meta.thumbnail.thumbnails[song_meta.thumbnail.thumbnails.length - 1] + .url }; let track_info = { - service: 'ytmusic', - type: 'track', + service: "ytmusic", + type: "track", id: song_meta.videoId, name: song_meta.title, streamUrl: `https://music.youtube.com/watch?v=${song_meta.videoId}`, purchaseUrl: null, artwork, artist: { - name: artists, + name: artists }, album: { - name: album_name, - }, - } + name: album_name + } + }; return Promise.resolve(track_info); } async function lookupAlbum(id) { - let request_body = {'browseEndpointContextSupportedConfigs': {'browseEndpointContextMusicConfig': {'pageType': 'MUSIC_PAGE_TYPE_ALBUM'}}, 'browseId': id, ...standard_body } + let request_body = { + browseEndpointContextSupportedConfigs: { + browseEndpointContextMusicConfig: { pageType: "MUSIC_PAGE_TYPE_ALBUM" } + }, + browseId: id, + ...standard_body + }; - const { body } = await request.post("https://music.youtube.com/youtubei/v1/browse") + const { body } = await request + .post("https://music.youtube.com/youtubei/v1/browse") .set(standard_headers) .query(standard_params) - .send(request_body) + .send(request_body); + + let data = body.frameworkUpdates?.entityBatchUpdate.mutations; - let data = body.frameworkUpdates?.entityBatchUpdate.mutations if (data === undefined) { - throw new Error() + throw new Error(); } - let album_data = data.find((entry) => { + let album_data = data.find(entry => { if (entry.payload.musicAlbumRelease !== undefined) { - return true + return true; } - return false + return false; }).payload.musicAlbumRelease; let artists; if (album_data.primaryArtists) { - artists= data.filter((entry) => { - if (entry.payload.musicArtist !== undefined) { - if (album_data.primaryArtists.includes(entry.entityKey)) { - return true + artists = data + .filter(entry => { + if (entry.payload.musicArtist !== undefined) { + if (album_data.primaryArtists.includes(entry.entityKey)) { + return true; + } } - } - return false - }).map((entry) => entry.payload.musicArtist.name); - } else { // Various artists, most likely + return false; + }) + .map(entry => entry.payload.musicArtist.name); + } else { + // Various artists, most likely artists = [album_data.artistDisplayName]; } const artwork = { small: album_data.thumbnailDetails.thumbnails[0].url, - large: album_data.thumbnailDetails.thumbnails[album_data.thumbnailDetails.thumbnails.length-1].url, + large: + album_data.thumbnailDetails.thumbnails[ + album_data.thumbnailDetails.thumbnails.length - 1 + ].url }; return Promise.resolve({ - service: 'ytmusic', - type: 'album', + service: "ytmusic", + type: "album", id, name: album_data.title, streamUrl: null, @@ -206,41 +296,42 @@ async function lookupAlbum(id) { purchaseUrl: null, artwork, artist: { - name: artists.join(", "), + name: artists.join(", ") }, playlistId: album_data.audioPlaylistId }); } async function lookupPlaylist(id) { - const endpoint = "https://music.youtube.com/playlist" - const response = await request.get(endpoint) + const endpoint = "https://music.youtube.com/playlist"; + const response = await request + .get(endpoint) .set(standard_headers) - .query({list: id}) - let match = response.text.match(/"MPRE[_a-zA-Z0-9]+/) - let albumId + .query({ list: id }); + let match = response.text.match(/"MPRE[_a-zA-Z0-9]+/); + let albumId; if (match) { - albumId = match[0].substr(1) + albumId = match[0].substr(1); } else { debug("Couldn't match album id"); throw new Error(); } - const possibleAlbum = await lookupAlbum(albumId) - if (possibleAlbum.playlistId = id) { + const possibleAlbum = await lookupAlbum(albumId); + if ((possibleAlbum.playlistId = id)) { return possibleAlbum; } throw new Error(); } export async function lookupId(id, type) { - if (type == 'track') { + if (type == "track") { return lookupTrack(id); - } else if (type == 'album') { + } else if (type == "album") { return lookupAlbum(id); - } else if (type == 'playlist') { + } else if (type == "playlist") { return lookupPlaylist(id); } - return { service: 'ytmusic', id }; + return { service: "ytmusic", id }; } export function parseUrl(url) { @@ -251,13 +342,15 @@ export function parseUrl(url) { let match; if (parsed.path.match(/^\/watch/) && id !== undefined) { - return lookupId(id, 'track'); - } else if (match = parsed.path.match(/^\/browse\/([A-Za-z0-9_]+)/)) { - return lookupId(match[1], 'album'); - } else if (match = parsed.path.match(/^\/playlist/) && list_id !== undefined) { - return lookupId(list_id, 'playlist'); + return lookupId(id, "track"); + } else if ((match = parsed.path.match(/^\/browse\/([A-Za-z0-9_]+)/))) { + return lookupId(match[1], "album"); + } else if ( + (match = parsed.path.match(/^\/playlist/) && list_id !== undefined) + ) { + return lookupId(list_id, "playlist"); } throw new Error(); } -export const id = 'ytmusic'; +export const id = "ytmusic"; export const match = urlMatch; diff --git a/test/services/deezer.js b/test/services/deezer.js index fec3929..3c84480 100644 --- a/test/services/deezer.js +++ b/test/services/deezer.js @@ -1,45 +1,62 @@ -import 'should'; -import * as deezer from '../../lib/services/deezer/index.js'; +import "should"; +import * as deezer from "../../lib/services/deezer/index.js"; -describe('Deezer', () => { - describe('lookupId', () => { - it('should find album by ID', async function test() { - const result = await deezer.lookupId('302127', 'album'); - result.name.should.equal('Discovery'); +describe("Deezer", () => { + describe("lookupId", () => { + it("should find album by ID", async function test() { + const result = await deezer.lookupId("302127", "album"); + result.name.should.equal("Discovery"); }); - it('should find track by ID', async function (){ - const result = await deezer.lookupId('3135554', 'track'); - result.name.should.equal('Aerodynamic'); + it("should find track by ID", async function() { + const result = await deezer.lookupId("3135554", "track"); + result.name.should.equal("Aerodynamic"); }); }); - describe('search', () => { - it('should find album by search', async function (){ - const result = await deezer.search({type: 'album', artist: {name: 'Jamie xx'}, name: 'In Colour'}); - result.name.should.startWith('In Colour'); + describe("search", () => { + it("should find album by search", async function() { + const result = await deezer.search({ + type: "album", + artist: { name: "Jamie xx" }, + name: "In Colour" + }); + result.name.should.startWith("In Colour"); }); - it('should find album with various artists by search', async function (){ - const result = await deezer.search({type: 'album', artist: {name: 'Various Artists'}, name: 'The Trevor Nelson Collection'}); - result.name.should.equal('The Trevor Nelson Collection'); + it("should find album with various artists by search", async function() { + const result = await deezer.search({ + type: "album", + artist: { name: "Various Artists" }, + name: "Rocket League x Monstercat Vol. 6" + }); + result.name.should.equal("Rocket League x Monstercat Vol. 6"); }); - it('should find track by search', async function (){ - const result = await deezer.search({type: 'track', artist: {name: 'Deftones'}, albumName: 'Deftones', name: 'Hexagram'}); - result.name.should.equal('Hexagram'); + it("should find track by search", async function() { + const result = await deezer.search({ + type: "track", + artist: { name: "Deftones" }, + albumName: "Deftones", + name: "Hexagram" + }); + result.name.should.equal("Hexagram"); }); }); - describe('lookupUrl', () => { - describe('parseUrl', () => { - it('should parse album url into ID', async function (){ - const result = await deezer.parseUrl('http://www.deezer.com/album/302127'); + describe("lookupUrl", () => { + describe("parseUrl", () => { + it("should parse album url into ID", async function() { + const result = await deezer.parseUrl( + "http://www.deezer.com/album/302127" + ); result.id.should.equal(302127); }); - it('should parse track url into ID', async function (){ - const result = await deezer.parseUrl('http://www.deezer.com/track/3135554'); + it("should parse track url into ID", async function() { + const result = await deezer.parseUrl( + "http://www.deezer.com/track/3135554" + ); result.id.should.equal(3135554); }); }); diff --git a/test/services/itunes.js b/test/services/itunes.js index 9a4a6bc..305523e 100644 --- a/test/services/itunes.js +++ b/test/services/itunes.js @@ -1,46 +1,54 @@ -import 'should'; -import * as itunes from '../../lib/services/itunes/index.js'; +import "should"; +import * as itunes from "../../lib/services/itunes/index.js"; -describe('iTunes Music', function(){ - describe('lookupId', function(){ - it('should find album by ID', async function (){ - const result = await itunes.lookupId('1445991287', 'album'); - result.name.should.equal('Peace Orchestra'); +describe("iTunes Music", function() { + describe("lookupId", function() { + it("should find album by ID", async function() { + const result = await itunes.lookupId("1445991287", "album"); + result.name.should.equal("Peace Orchestra"); }); - it('should find track by ID', async function (){ - const result = await itunes.lookupId('1445927701', 'track'); - result.name.should.equal('Double Drums'); + it("should find track by ID", async function() { + const result = await itunes.lookupId("1440718994", "track"); + result.name.should.equal("Zombie"); }); }); - describe('search', function(){ - it('should find album by search', async function (){ - const result = await itunes.search({type: 'album', artist: {name: 'Deftones'}, name: 'White Pony'}); - result.name.should.equal('White Pony'); + describe("search", function() { + it("should find album by search", async function() { + const result = await itunes.search({ + type: "album", + artist: { name: "Deftones" }, + name: "White Pony" + }); + result.name.should.equal("White Pony"); }); - it('should find awkward album by search', async function (){ - const result = await itunes.search({type: 'album', artist: {name: 'Anavitória'}, name: 'Fica'}); - result.name.should.equal('Fica (feat. Matheus & Kauan) - Single'); - }); - - it('should find track by search', async function (){ - const result = await itunes.search({type: 'track', artist: {name: 'Deftones'}, albumName: 'Deftones', name: 'Hexagram'}); - result.name.should.equal('Hexagram'); + it("should find track by search", async function() { + const result = await itunes.search({ + type: "track", + artist: { name: "Deftones" }, + albumName: "Deftones", + name: "Hexagram" + }); + result.name.should.equal("Hexagram"); }); }); - describe('lookupUrl', function(){ - describe('parseUrl', function(){ - it('should parse album url into ID', async function (){ - const result = await itunes.parseUrl('https://itunes.apple.com/us/album/peace-orchestra/1445991287'); - result.id.should.equal('us1445991287'); + describe("lookupUrl", function() { + describe("parseUrl", function() { + it("should parse album url into ID", async function() { + const result = await itunes.parseUrl( + "https://itunes.apple.com/us/album/peace-orchestra/1445991287" + ); + result.id.should.equal("us1445991287"); }); - it('should parse track url into ID', async function (){ - const result = await itunes.parseUrl('https://itunes.apple.com/us/album/double-drums-dj-dsl-mix/1445927689?i=1445927701'); - result.id.should.equal('us1445927689'); + it("should parse track url into ID", async function() { + const result = await itunes.parseUrl( + "https://itunes.apple.com/us/album/double-drums-dj-dsl-mix/1445927689?i=1445927701" + ); + result.id.should.equal("us1445927689"); }); }); }); diff --git a/test/services/ytmusic.js b/test/services/ytmusic.js index 4aeae9e..2a56895 100644 --- a/test/services/ytmusic.js +++ b/test/services/ytmusic.js @@ -1,79 +1,111 @@ -import 'should'; -import * as ytmusic from '../../lib/services/ytmusic/index.js'; +import "should"; +import * as ytmusic from "../../lib/services/ytmusic/index.js"; -describe('ytmusic', function(){ - describe('lookupId', () => { - it('should find album by ID', async function testV() { - const result = await ytmusic.lookupId('MPREb_nlOKEssnatr', 'album'); - result.name.should.equal('Carne de Pescoço'); +describe("ytmusic", function() { + describe("lookupId", () => { + it("should find album by ID", async function testV() { + const result = await ytmusic.lookupId( + "OLAK5uy_kalBoqyqQmbtZKCBV43Qipcoe2O2Hg_to", + "album" + ); + result.name.should.equal("Carne de Pescoço"); }); - it('should find track by ID', async function (){ - const result = await ytmusic.lookupId('9zrYXvUXiQk', 'track'); - result.name.should.equal('One Vision'); - result.artist.name.should.equal('Queen'); - result.album.name.should.equal('A Kind Of Magic') + it("should find track by ID", async function() { + const result = await ytmusic.lookupId("9zrYXvUXiQk", "track"); + result.name.should.equal("One Vision"); + result.artist.name.should.equal("Queen"); + result.album.name.should.equal("A Kind Of Magic"); }); - it('should find track by ID', async function (){ - const result = await ytmusic.lookupId('rAzfNuU1f8E', 'track'); - result.name.should.equal('Erre (Live)'); - result.artist.name.should.equal('Boogarins'); + it("should find track by ID", async function() { + const result = await ytmusic.lookupId("rAzfNuU1f8E", "track"); + result.name.should.equal("Erre (Live)"); + result.artist.name.should.equal("Boogarins"); // The copyright notice is too long and is the only place where the album name is. - result.album.name.should.equal('') + result.album.name.should.equal(""); }); - it('should find track by ID', async function (){ - const result = await ytmusic.lookupId('Wst0la_TgTY', 'track'); - result.name.should.equal('Às Vezes Bate Uma Saudade'); + it("should find track by ID", async function() { + const result = await ytmusic.lookupId("Wst0la_TgTY", "track"); + result.name.should.equal("Às Vezes Bate Uma Saudade"); // XXX: This is very odd. Sometimes, google will return the first artist "Rodrigo Alarcon", sometimes "Rodrigo Alarcon, Ana Muller & Mariana Froes" and sometimes // "Rodrigo Alarcon, Ana Muller, Mariana Froes". Same API call, same everything. Go figure. // result.artist.name.should.equal('Rodrigo Alarcon, Ana Muller, Mariana Froes'); - result.artist.name.should.startWith('Rodrigo Alarcon'); - result.album.name.should.equal('Taquetá Vol.1') + result.artist.name.should.startWith("Rodrigo Alarcon"); + result.album.name.should.equal("Taquetá Vol.1"); }); }); - describe('search', () => { - it('should find album by search', async function (){ - const result = await ytmusic.search({type: 'album', artist: {name: 'Jamie xx'}, name: 'In Colour'}); - result.name.should.startWith('In Colour'); + describe("search", () => { + it("should find album by search", async function() { + const result = await ytmusic.search({ + type: "album", + artist: { name: "Jamie xx" }, + name: "In Colour" + }); + result.name.should.startWith("In Colour"); result.id.should.equal("MPREb_IbDz5pAZFvJ"); }); - it('should find album with various artists by search', async function (){ - const result = await ytmusic.search({type: 'album', artist: {name: 'Various Artists'}, name: 'Sambabook João Nogueira'}); - result.name.should.equal('Sambabook João Nogueira'); - result.id.should.equal('MPREb_iZt1VjORlv7'); + it("should find album with various artists by search", async function() { + const result = await ytmusic.search({ + type: "album", + artist: { name: "Various Artists" }, + name: "Sambabook João Nogueira" + }); + result.name.should.equal("Sambabook João Nogueira"); + result.id.should.equal("MPREb_iZt1VjORlv7"); }); - it('should find album and make sure it makes sense by search', async function(){ - const result = await ytmusic.search({type: 'album', artist: {name: 'The Beatles'}, name: 'The Beatles'}); - result.name.should.equal('The Beatles'); - result.id.should.equal('MPREb_S5TiUIYvI78'); + it("should find album and make sure it makes sense by search", async function() { + const result = await ytmusic.search({ + type: "album", + artist: { name: "The Beatles" }, + name: "The Beatles" + }); + result.name.should.equal("The Beatles"); + result.id.should.equal("MPREb_S5TiUIYvI78"); }); - it('should find track by search', async function (){ - const result = await ytmusic.search({type: 'track', artist: {name: 'Oasis'}, albumName: 'Stop The Clocks', name: 'Wonderwall'}); - result.name.should.equal('Wonderwall'); - result.id.should.equal('Gvfgut8nAgw'); + it("should find track by search", async function() { + const result = await ytmusic.search({ + type: "track", + artist: { name: "Oasis" }, + albumName: "Stop The Clocks", + name: "Wonderwall" + }); + result.name.should.equal("Wonderwall"); + result.id.should.equal("Gvfgut8nAgw"); }); }); - describe('lookupUrl', () => { - describe('parseUrl', () => { - it('should parse track url into ID', async function (){ - const result = await ytmusic.parseUrl('https://music.youtube.com/watch?v=YLp2cW7ICCU&feature=share'); + describe("lookupUrl", () => { + describe("parseUrl", () => { + it("should parse track url into ID", async function() { + const result = await ytmusic.parseUrl( + "https://music.youtube.com/watch?v=YLp2cW7ICCU&feature=share" + ); result.id.should.equal("YLp2cW7ICCU"); - result.streamUrl.should.equal("https://music.youtube.com/watch?v=YLp2cW7ICCU"); + result.streamUrl.should.equal( + "https://music.youtube.com/watch?v=YLp2cW7ICCU" + ); }); - it('should parse album url into ID', async function (){ - const result = await ytmusic.parseUrl('https://music.youtube.com/browse/MPREb_9C36yscfgmJ'); + it("should parse album url into ID", async function() { + const result = await ytmusic.parseUrl( + "https://music.youtube.com/browse/MPREb_9C36yscfgmJ" + ); result.id.should.equal("MPREb_9C36yscfgmJ"); - result.streamUrl.should.equal("https://music.youtube.com/browse/MPREb_9C36yscfgmJ"); + result.streamUrl.should.equal( + "https://music.youtube.com/browse/MPREb_9C36yscfgmJ" + ); }); - it('should parse alternative album url into ID', async function (){ - const result = await ytmusic.parseUrl('https://music.youtube.com/playlist?list=OLAK5uy_lx9K5RpiBEwd3E4C1GKqY7e06qTlwydvs'); + it("should parse alternative album url into ID", async function() { + const result = await ytmusic.parseUrl( + "https://music.youtube.com/playlist?list=OLAK5uy_lx9K5RpiBEwd3E4C1GKqY7e06qTlwydvs" + ); result.id.should.equal("MPREb_9C36yscfgmJ"); }); - it('should parse alternative album url into ID, regression', async function (){ - const result = await ytmusic.parseUrl('https://music.youtube.com/playlist?list=OLAK5uy_kxepMtCUKFek54-bgWICIsmglK86HD0TM'); + it("should parse alternative album url into ID, regression", async function() { + const result = await ytmusic.parseUrl( + "https://music.youtube.com/playlist?list=OLAK5uy_kxepMtCUKFek54-bgWICIsmglK86HD0TM" + ); result.id.should.equal("MPREb_XmlDLpyWvMt"); }); });