Some service api fixes

This commit is contained in:
Jonathan Cremin 2025-05-20 13:18:38 +01:00
parent 2225712d5f
commit 3ea715de39
6 changed files with 425 additions and 249 deletions

View file

@ -1,28 +1,30 @@
import { parse } from 'url'; import { parse } from "url";
import querystring from 'querystring'; import querystring from "querystring";
import request from 'superagent'; import request from "superagent";
import urlMatch from './url.js'; import urlMatch from "./url.js";
const apiRoot = 'https://itunes.apple.com'; const apiRoot = "https://itunes.apple.com";
export async function parseUrl(url) { export async function parseUrl(url) {
const parsed = parse(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); const query = querystring.parse(parsed.query);
let itunesId = matches[3]; let itunesId = matches[4];
if (matches) { if (matches) {
let type = 'album'; let type = "album";
if (matches[3].match(/^id/)) { if (matches[3].match(/^id/)) {
itunesId = matches[3].substr(2); itunesId = matches[3].substr(2);
if (query.i) { if (query.i) {
type = 'track'; type = "track";
itunesId = query.i; itunesId = query.i;
} }
} }
return await lookupId(itunesId, type, matches[1] || 'us'); return await lookupId(itunesId, type, matches[1] || "us");
} }
throw new Error(); throw new Error();
} }
@ -44,61 +46,74 @@ export async function lookupId(possibleId, type, countrycode) {
const response = await request.get(apiRoot + path); const response = await request.get(apiRoot + path);
let result = JSON.parse(response.text); 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(); throw new Error();
} else { } else {
result = result.results[0]; result = result.results[0];
const item = { const item = {
service: 'itunes', service: "itunes",
type, type,
id: cc + id, id: cc + id,
name: result.trackName ? result.trackName : result.collectionName, name: result.trackName ? result.trackName : result.collectionName,
streamUrl: null, streamUrl: null,
purchaseUrl: result.collectionViewUrl, purchaseUrl: result.collectionViewUrl,
artwork: { artwork: {
small: `${result.artworkUrl100.replace('100x100', '200x200').replace('.mzstatic.com', '.mzstatic.com').replace('http://', 'https://')}`, small: `${result.artworkUrl100
large: `${result.artworkUrl100.replace('100x100', '600x600').replace('.mzstatic.com', '.mzstatic.com').replace('http://', 'https://')}`, .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: { artist: {
name: result.artistName, name: result.artistName
}, }
}; };
if (type === 'track') { if (type === "track") {
item.album = { item.album = {
name: result.collectionName, name: result.collectionName
}; };
} }
return item; return item;
} }
} catch (e) { } catch (e) {
const error = new Error('Not Found'); const error = new Error("Not Found");
error.status = 404; error.status = 404;
return Promise.reject(error); return Promise.reject(error);
} }
} }
export async function search(data) { export async function search(data) {
const markets = ['us', 'gb', 'jp', 'br', 'de', 'es']; const markets = ["us", "gb", "jp", "br", "de", "es"];
let query; let query;
let album; let album;
let entity; let entity;
const type = data.type; const type = data.type;
if (type === 'album') { if (type === "album") {
query = `${data.artist.name} ${data.name}`; query = `${data.artist.name} ${data.name}`;
album = data.name; album = data.name;
entity = 'album'; entity = "album";
} else if (type === 'track') { } else if (type === "track") {
query = `${data.artist.name} ${data.albumName} ${data.name}`; query = `${data.artist.name} ${data.albumName} ${data.name}`;
album = data.albumName; album = data.albumName;
entity = 'musicTrack'; entity = "musicTrack";
} }
for (const market of markets) { // eslint-disable-line for (const market of markets) {
const path = `/${market}/search?term=${encodeURIComponent(query)}&media=music&entity=${entity}`; // eslint-disable-line
const path = `/${market}/search?term=${encodeURIComponent(
query
)}&media=music&entity=${entity}`;
const response = await request.get(apiRoot + path); const response = await request.get(apiRoot + path);
let result = JSON.parse(response.text); let result = JSON.parse(response.text);
@ -106,9 +121,9 @@ export async function search(data) {
const matches = album.match(/^[^([]+/); const matches = album.match(/^[^([]+/);
if (matches && matches[0] && matches[0] !== album) { if (matches && matches[0] && matches[0] !== album) {
const cleanedData = JSON.parse(JSON.stringify(data)); const cleanedData = JSON.parse(JSON.stringify(data));
if (type === 'album') { if (type === "album") {
cleanedData.name = matches[0].trim(); cleanedData.name = matches[0].trim();
} else if (type === 'track') { } else if (type === "track") {
cleanedData.albumName = matches[0].trim(); cleanedData.albumName = matches[0].trim();
} }
return await search(cleanedData); return await search(cleanedData);
@ -117,31 +132,37 @@ export async function search(data) {
result = result.results[0]; result = result.results[0];
const item = { const item = {
service: 'itunes', service: "itunes",
type, type,
id: `us${result.collectionId}`, id: `us${result.collectionId}`,
name: result.trackName ? result.trackName : result.collectionName, name: result.trackName ? result.trackName : result.collectionName,
streamUrl: result.collectionViewUrl, streamUrl: result.collectionViewUrl,
purchaseUrl: result.collectionViewUrl, purchaseUrl: result.collectionViewUrl,
artwork: { artwork: {
small: `${result.artworkUrl100.replace('100x100', '200x200').replace('.mzstatic.com', '.mzstatic.com').replace('http://', 'https://')}`, small: `${result.artworkUrl100
large: `${result.artworkUrl100.replace('100x100', '600x600').replace('.mzstatic.com', '.mzstatic.com').replace('http://', 'https://')}`, .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: { artist: {
name: result.artistName, name: result.artistName
}, }
}; };
if (type === 'track') { if (type === "track") {
item.album = { item.album = {
name: result.collectionName, name: result.collectionName
}; };
} }
return item; return item;
} }
} }
return { service: 'itunes' }; return { service: "itunes" };
} }
export const id = 'itunes'; export const id = "itunes";
export const match = urlMatch; export const match = urlMatch;

View file

@ -1,13 +1,18 @@
import { parse } from 'url'; import { parse } from "url";
export default function match(url) { export default function match(url) {
const parsed = parse(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; 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];
} }

View file

@ -1,73 +1,133 @@
import urlMatch from './url.js'; import urlMatch from "./url.js";
import querystring from 'querystring'; import querystring from "querystring";
import request from 'superagent'; import request from "superagent";
import { parse } from 'url'; import { parse } from "url";
import debuglog from 'debug'; 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 = { const standard_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0", "User-Agent":
"Accept": "*/*", "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", "Accept-Language": "en-US,en;q=0.5",
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Goog-AuthUser": "0", "X-Goog-AuthUser": "0",
"origin": "https://music.youtube.com", origin: "https://music.youtube.com",
"X-Goog-Visitor-Id": "CgtWaTB2WWRDeEFUYyjhv-X8BQ%3D%3D" "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 base_filter = "Eg-KAQwIA";
const albums_filter = "BAAGAEgACgA" const albums_filter = "BAAGAEgACgA";
const tracks_filter = "RAAGAAgACgA" const tracks_filter = "RAAGAAgACgA";
// If you make a typo, ytmusic searches for a correction. With this filter it will look for the exact match // 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 // 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 // The logic here comes from https://github.com/sigma67/ytmusicapi
// If something doesn't work, looking up back there might be a good idea. // If something doesn't work, looking up back there might be a good idea.
export async function search(data, original = {}) { export async function search(data, original = {}) {
let query; 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) { if (various) {
data.artist.name = ""; data.artist.name = "";
} }
if (data.type == "track") { 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") { } else if (data.type == "album") {
query = [data.name, data.artist.name] query = [data.name, data.artist.name];
} else { } else {
throw new Error(); throw new Error();
} }
// Add "" to try and make the search better, works for stuff like "The beatles" to reduce noise // 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 params =
let request_body = {query, params, ...standard_body } 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") try {
const { body } = await request
.post("https://music.youtube.com/youtubei/v1/search")
.set(standard_headers) .set(standard_headers)
.query(standard_params) .query(standard_params)
.send(request_body) .send(request_body);
} catch (err) {
debug(err);
}
// no results // no results
if (body.contents === undefined) { if (body.contents === undefined) {
debug("Empty body, no results") debug("Empty body, no results");
return { service: 'ytmusic' }; return { service: "ytmusic" };
} }
let results; let results;
if (body.contents.tabbedSearchResultsRenderer !== undefined) { if (body.contents.tabbedSearchResultsRenderer !== undefined) {
results = body.contents.tabbedSearchResultsRenderer.tabs[0].tabRenderer.content results =
body.contents.tabbedSearchResultsRenderer.tabs[0].tabRenderer.content;
} else { } else {
results = body.contents.sectionListRenderer.contents results = body.contents.sectionListRenderer.contents;
} }
// no results // no results
if (results.length == 1 && results.itemSectionRenderer !== undefined) { if (results.length == 1 && results.itemSectionRenderer !== undefined) {
debug("Only itemSectionRenderer, no results") debug("Only itemSectionRenderer, no results");
return { service: 'ytmusic' }; return { service: "ytmusic" };
} }
for (const result of results) { for (const result of results) {
@ -75,130 +135,160 @@ export async function search(data, original = {}) {
continue; 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. // 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 // 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) { 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 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) { if (nameMatch && artistMatch) {
return possibleMatch return possibleMatch;
} }
} }
} }
debug("Finished looking up, no results") debug("Finished looking up, no results");
return { service: 'ytmusic' }; return { service: "ytmusic" };
} }
function parse_result_content(contents, type) { function parse_result_content(contents, type) {
let matches = [] let matches = [];
for (const result of contents) { for (const result of contents) {
const data = result.musicResponsiveListItemRenderer; 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)) { if (["Video", "Playlist"].includes(informed_type)) {
continue; continue;
} }
let matchId; let matchId;
if (type == "track") { 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") { } else if (type == "album") {
matchId = data.navigationEndpoint?.browseEndpoint.browseId matchId = data.navigationEndpoint?.browseEndpoint.browseId;
} }
if (matchId) { if (matchId) {
matches.push(matchId) matches.push(matchId);
} }
} }
return matches return matches;
} }
async function lookupTrack(id) { 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) .set(standard_headers)
.query(standard_params) .query(standard_params)
.send(request_body) .send(request_body);
let song_meta = body.videoDetails let song_meta = body.videoDetails;
let description = body.microformat.microformatDataRenderer.description.split(' · ') let description = body.microformat.microformatDataRenderer.description.split(
let possible_album_name = description[description.length - 1].split("℗")[0] " · "
);
let possible_album_name = description[description.length - 1].split("℗")[0];
if (!description[description.length - 1].includes("℗")) { if (!description[description.length - 1].includes("℗")) {
possible_album_name = ""; possible_album_name = "";
} }
let tags = body.microformat.microformatDataRenderer.tags let tags = body.microformat.microformatDataRenderer.tags;
let album_name = "" let album_name = "";
for (const tag of tags) { for (const tag of tags) {
if (possible_album_name.includes(tag)) { if (possible_album_name.includes(tag)) {
album_name = tag; album_name = tag;
} }
} }
let artists = song_meta.author let artists = song_meta.author;
artists = artists.replace(" - Topic", "") artists = artists.replace(" - Topic", "");
const artwork = { const artwork = {
small: song_meta.thumbnail.thumbnails[0].url, 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 = { let track_info = {
service: 'ytmusic', service: "ytmusic",
type: 'track', type: "track",
id: song_meta.videoId, id: song_meta.videoId,
name: song_meta.title, name: song_meta.title,
streamUrl: `https://music.youtube.com/watch?v=${song_meta.videoId}`, streamUrl: `https://music.youtube.com/watch?v=${song_meta.videoId}`,
purchaseUrl: null, purchaseUrl: null,
artwork, artwork,
artist: { artist: {
name: artists, name: artists
}, },
album: { album: {
name: album_name, name: album_name
},
} }
};
return Promise.resolve(track_info); return Promise.resolve(track_info);
} }
async function lookupAlbum(id) { 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) .set(standard_headers)
.query(standard_params) .query(standard_params)
.send(request_body) .send(request_body);
let data = body.frameworkUpdates?.entityBatchUpdate.mutations;
let data = body.frameworkUpdates?.entityBatchUpdate.mutations
if (data === undefined) { 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) { if (entry.payload.musicAlbumRelease !== undefined) {
return true return true;
} }
return false return false;
}).payload.musicAlbumRelease; }).payload.musicAlbumRelease;
let artists; let artists;
if (album_data.primaryArtists) { if (album_data.primaryArtists) {
artists= data.filter((entry) => { artists = data
.filter(entry => {
if (entry.payload.musicArtist !== undefined) { if (entry.payload.musicArtist !== undefined) {
if (album_data.primaryArtists.includes(entry.entityKey)) { if (album_data.primaryArtists.includes(entry.entityKey)) {
return true return true;
} }
} }
return false return false;
}).map((entry) => entry.payload.musicArtist.name); })
} else { // Various artists, most likely .map(entry => entry.payload.musicArtist.name);
} else {
// Various artists, most likely
artists = [album_data.artistDisplayName]; artists = [album_data.artistDisplayName];
} }
const artwork = { const artwork = {
small: album_data.thumbnailDetails.thumbnails[0].url, 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({ return Promise.resolve({
service: 'ytmusic', service: "ytmusic",
type: 'album', type: "album",
id, id,
name: album_data.title, name: album_data.title,
streamUrl: null, streamUrl: null,
@ -206,41 +296,42 @@ async function lookupAlbum(id) {
purchaseUrl: null, purchaseUrl: null,
artwork, artwork,
artist: { artist: {
name: artists.join(", "), name: artists.join(", ")
}, },
playlistId: album_data.audioPlaylistId playlistId: album_data.audioPlaylistId
}); });
} }
async function lookupPlaylist(id) { async function lookupPlaylist(id) {
const endpoint = "https://music.youtube.com/playlist" const endpoint = "https://music.youtube.com/playlist";
const response = await request.get(endpoint) const response = await request
.get(endpoint)
.set(standard_headers) .set(standard_headers)
.query({list: id}) .query({ list: id });
let match = response.text.match(/"MPRE[_a-zA-Z0-9]+/) let match = response.text.match(/"MPRE[_a-zA-Z0-9]+/);
let albumId let albumId;
if (match) { if (match) {
albumId = match[0].substr(1) albumId = match[0].substr(1);
} else { } else {
debug("Couldn't match album id"); debug("Couldn't match album id");
throw new Error(); throw new Error();
} }
const possibleAlbum = await lookupAlbum(albumId) const possibleAlbum = await lookupAlbum(albumId);
if (possibleAlbum.playlistId = id) { if ((possibleAlbum.playlistId = id)) {
return possibleAlbum; return possibleAlbum;
} }
throw new Error(); throw new Error();
} }
export async function lookupId(id, type) { export async function lookupId(id, type) {
if (type == 'track') { if (type == "track") {
return lookupTrack(id); return lookupTrack(id);
} else if (type == 'album') { } else if (type == "album") {
return lookupAlbum(id); return lookupAlbum(id);
} else if (type == 'playlist') { } else if (type == "playlist") {
return lookupPlaylist(id); return lookupPlaylist(id);
} }
return { service: 'ytmusic', id }; return { service: "ytmusic", id };
} }
export function parseUrl(url) { export function parseUrl(url) {
@ -251,13 +342,15 @@ export function parseUrl(url) {
let match; let match;
if (parsed.path.match(/^\/watch/) && id !== undefined) { if (parsed.path.match(/^\/watch/) && id !== undefined) {
return lookupId(id, 'track'); return lookupId(id, "track");
} else if (match = parsed.path.match(/^\/browse\/([A-Za-z0-9_]+)/)) { } else if ((match = parsed.path.match(/^\/browse\/([A-Za-z0-9_]+)/))) {
return lookupId(match[1], 'album'); return lookupId(match[1], "album");
} else if (match = parsed.path.match(/^\/playlist/) && list_id !== undefined) { } else if (
return lookupId(list_id, 'playlist'); (match = parsed.path.match(/^\/playlist/) && list_id !== undefined)
) {
return lookupId(list_id, "playlist");
} }
throw new Error(); throw new Error();
} }
export const id = 'ytmusic'; export const id = "ytmusic";
export const match = urlMatch; export const match = urlMatch;

View file

@ -1,45 +1,62 @@
import 'should'; import "should";
import * as deezer from '../../lib/services/deezer/index.js'; import * as deezer from "../../lib/services/deezer/index.js";
describe('Deezer', () => { describe("Deezer", () => {
describe('lookupId', () => { describe("lookupId", () => {
it('should find album by ID', async function test() { it("should find album by ID", async function test() {
const result = await deezer.lookupId('302127', 'album'); const result = await deezer.lookupId("302127", "album");
result.name.should.equal('Discovery'); result.name.should.equal("Discovery");
}); });
it('should find track by ID', async function (){ it("should find track by ID", async function() {
const result = await deezer.lookupId('3135554', 'track'); const result = await deezer.lookupId("3135554", "track");
result.name.should.equal('Aerodynamic'); result.name.should.equal("Aerodynamic");
}); });
}); });
describe('search', () => { describe("search", () => {
it('should find album by search', async function (){ it("should find album by search", async function() {
const result = await deezer.search({type: 'album', artist: {name: 'Jamie xx'}, name: 'In Colour'}); const result = await deezer.search({
result.name.should.startWith('In Colour'); 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 (){ 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'}); const result = await deezer.search({
result.name.should.equal('The Trevor Nelson Collection'); 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 (){ it("should find track by search", async function() {
const result = await deezer.search({type: 'track', artist: {name: 'Deftones'}, albumName: 'Deftones', name: 'Hexagram'}); const result = await deezer.search({
result.name.should.equal('Hexagram'); type: "track",
artist: { name: "Deftones" },
albumName: "Deftones",
name: "Hexagram"
});
result.name.should.equal("Hexagram");
}); });
}); });
describe('lookupUrl', () => { describe("lookupUrl", () => {
describe('parseUrl', () => { describe("parseUrl", () => {
it('should parse album url into ID', async function (){ it("should parse album url into ID", async function() {
const result = await deezer.parseUrl('http://www.deezer.com/album/302127'); const result = await deezer.parseUrl(
"http://www.deezer.com/album/302127"
);
result.id.should.equal(302127); result.id.should.equal(302127);
}); });
it('should parse track url into ID', async function (){ it("should parse track url into ID", async function() {
const result = await deezer.parseUrl('http://www.deezer.com/track/3135554'); const result = await deezer.parseUrl(
"http://www.deezer.com/track/3135554"
);
result.id.should.equal(3135554); result.id.should.equal(3135554);
}); });
}); });

View file

@ -1,46 +1,54 @@
import 'should'; import "should";
import * as itunes from '../../lib/services/itunes/index.js'; import * as itunes from "../../lib/services/itunes/index.js";
describe('iTunes Music', function(){ describe("iTunes Music", function() {
describe('lookupId', function(){ describe("lookupId", function() {
it('should find album by ID', async function (){ it("should find album by ID", async function() {
const result = await itunes.lookupId('1445991287', 'album'); const result = await itunes.lookupId("1445991287", "album");
result.name.should.equal('Peace Orchestra'); result.name.should.equal("Peace Orchestra");
}); });
it('should find track by ID', async function (){ it("should find track by ID", async function() {
const result = await itunes.lookupId('1445927701', 'track'); const result = await itunes.lookupId("1440718994", "track");
result.name.should.equal('Double Drums'); result.name.should.equal("Zombie");
}); });
}); });
describe('search', function(){ describe("search", function() {
it('should find album by search', async function (){ it("should find album by search", async function() {
const result = await itunes.search({type: 'album', artist: {name: 'Deftones'}, name: 'White Pony'}); const result = await itunes.search({
result.name.should.equal('White Pony'); type: "album",
artist: { name: "Deftones" },
name: "White Pony"
});
result.name.should.equal("White Pony");
}); });
it('should find awkward album by search', async function (){ it("should find track by search", async function() {
const result = await itunes.search({type: 'album', artist: {name: 'Anavitória'}, name: 'Fica'}); const result = await itunes.search({
result.name.should.equal('Fica (feat. Matheus & Kauan) - Single'); 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("lookupUrl", function() {
describe('parseUrl', function(){ describe("parseUrl", function() {
it('should parse album url into ID', async function (){ it("should parse album url into ID", async function() {
const result = await itunes.parseUrl('https://itunes.apple.com/us/album/peace-orchestra/1445991287'); const result = await itunes.parseUrl(
result.id.should.equal('us1445991287'); "https://itunes.apple.com/us/album/peace-orchestra/1445991287"
);
result.id.should.equal("us1445991287");
}); });
it('should parse track url into ID', async function (){ 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'); const result = await itunes.parseUrl(
result.id.should.equal('us1445927689'); "https://itunes.apple.com/us/album/double-drums-dj-dsl-mix/1445927689?i=1445927701"
);
result.id.should.equal("us1445927689");
}); });
}); });
}); });

View file

@ -1,79 +1,111 @@
import 'should'; import "should";
import * as ytmusic from '../../lib/services/ytmusic/index.js'; import * as ytmusic from "../../lib/services/ytmusic/index.js";
describe('ytmusic', function(){ describe("ytmusic", function() {
describe('lookupId', () => { describe("lookupId", () => {
it('should find album by ID', async function testV() { it("should find album by ID", async function testV() {
const result = await ytmusic.lookupId('MPREb_nlOKEssnatr', 'album'); const result = await ytmusic.lookupId(
result.name.should.equal('Carne de Pescoço'); "OLAK5uy_kalBoqyqQmbtZKCBV43Qipcoe2O2Hg_to",
"album"
);
result.name.should.equal("Carne de Pescoço");
}); });
it('should find track by ID', async function (){ it("should find track by ID", async function() {
const result = await ytmusic.lookupId('9zrYXvUXiQk', 'track'); const result = await ytmusic.lookupId("9zrYXvUXiQk", "track");
result.name.should.equal('One Vision'); result.name.should.equal("One Vision");
result.artist.name.should.equal('Queen'); result.artist.name.should.equal("Queen");
result.album.name.should.equal('A Kind Of Magic') result.album.name.should.equal("A Kind Of Magic");
}); });
it('should find track by ID', async function (){ it("should find track by ID", async function() {
const result = await ytmusic.lookupId('rAzfNuU1f8E', 'track'); const result = await ytmusic.lookupId("rAzfNuU1f8E", "track");
result.name.should.equal('Erre (Live)'); result.name.should.equal("Erre (Live)");
result.artist.name.should.equal('Boogarins'); result.artist.name.should.equal("Boogarins");
// The copyright notice is too long and is the only place where the album name is. // 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 (){ it("should find track by ID", async function() {
const result = await ytmusic.lookupId('Wst0la_TgTY', 'track'); const result = await ytmusic.lookupId("Wst0la_TgTY", "track");
result.name.should.equal('Às Vezes Bate Uma Saudade'); 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 // 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. // "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.equal('Rodrigo Alarcon, Ana Muller, Mariana Froes');
result.artist.name.should.startWith('Rodrigo Alarcon'); result.artist.name.should.startWith("Rodrigo Alarcon");
result.album.name.should.equal('Taquetá Vol.1') result.album.name.should.equal("Taquetá Vol.1");
}); });
}); });
describe('search', () => { describe("search", () => {
it('should find album by search', async function (){ it("should find album by search", async function() {
const result = await ytmusic.search({type: 'album', artist: {name: 'Jamie xx'}, name: 'In Colour'}); const result = await ytmusic.search({
result.name.should.startWith('In Colour'); type: "album",
artist: { name: "Jamie xx" },
name: "In Colour"
});
result.name.should.startWith("In Colour");
result.id.should.equal("MPREb_IbDz5pAZFvJ"); result.id.should.equal("MPREb_IbDz5pAZFvJ");
}); });
it('should find album with various artists by search', async function (){ 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'}); const result = await ytmusic.search({
result.name.should.equal('Sambabook João Nogueira'); type: "album",
result.id.should.equal('MPREb_iZt1VjORlv7'); 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(){ 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'}); const result = await ytmusic.search({
result.name.should.equal('The Beatles'); type: "album",
result.id.should.equal('MPREb_S5TiUIYvI78'); 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 (){ it("should find track by search", async function() {
const result = await ytmusic.search({type: 'track', artist: {name: 'Oasis'}, albumName: 'Stop The Clocks', name: 'Wonderwall'}); const result = await ytmusic.search({
result.name.should.equal('Wonderwall'); type: "track",
result.id.should.equal('Gvfgut8nAgw'); artist: { name: "Oasis" },
albumName: "Stop The Clocks",
name: "Wonderwall"
});
result.name.should.equal("Wonderwall");
result.id.should.equal("Gvfgut8nAgw");
}); });
}); });
describe('lookupUrl', () => { describe("lookupUrl", () => {
describe('parseUrl', () => { describe("parseUrl", () => {
it('should parse track url into ID', async function (){ it("should parse track url into ID", async function() {
const result = await ytmusic.parseUrl('https://music.youtube.com/watch?v=YLp2cW7ICCU&feature=share'); const result = await ytmusic.parseUrl(
"https://music.youtube.com/watch?v=YLp2cW7ICCU&feature=share"
);
result.id.should.equal("YLp2cW7ICCU"); 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 (){ it("should parse album url into ID", async function() {
const result = await ytmusic.parseUrl('https://music.youtube.com/browse/MPREb_9C36yscfgmJ'); const result = await ytmusic.parseUrl(
"https://music.youtube.com/browse/MPREb_9C36yscfgmJ"
);
result.id.should.equal("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 (){ it("should parse alternative album url into ID", async function() {
const result = await ytmusic.parseUrl('https://music.youtube.com/playlist?list=OLAK5uy_lx9K5RpiBEwd3E4C1GKqY7e06qTlwydvs'); const result = await ytmusic.parseUrl(
"https://music.youtube.com/playlist?list=OLAK5uy_lx9K5RpiBEwd3E4C1GKqY7e06qTlwydvs"
);
result.id.should.equal("MPREb_9C36yscfgmJ"); result.id.should.equal("MPREb_9C36yscfgmJ");
}); });
it('should parse alternative album url into ID, regression', async function (){ 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'); const result = await ytmusic.parseUrl(
"https://music.youtube.com/playlist?list=OLAK5uy_kxepMtCUKFek54-bgWICIsmglK86HD0TM"
);
result.id.should.equal("MPREb_XmlDLpyWvMt"); result.id.should.equal("MPREb_XmlDLpyWvMt");
}); });
}); });