Add search method and tests (WIP)

Working for most cases, still having troubles with "various artists"
This commit is contained in:
Renato "Lond" Cerqueira 2021-01-17 02:03:23 +01:00
parent 2119ac5c4d
commit 8dcb1cbecc
2 changed files with 123 additions and 15 deletions

View file

@ -6,6 +6,102 @@ import debuglog from 'debug';
const debug = debuglog('combine.fm:ytmusic');
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": "*/*",
"Accept-Language": "en-US,en;q=0.5",
"Content-Type": "application/json",
"X-Goog-AuthUser": "0",
"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 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"
// 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';
if (various) {
data.artist.name = undefined;
}
if (data.type == "track") {
query = [data.name, data.artist.name, data.albumName]
} else if (data.type == "album") {
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(" ")
let param = base_filter + (data.type == "track" ? tracks_filter : albums_filter) + exact_search_filter
let request_body = {query, param, ...standard_body }
const { body } = await request.post("https://music.youtube.com/youtubei/v1/search")
.set(standard_headers)
.query(standard_params)
.send(request_body)
// no results
if (body.contents === undefined) {
debug("Empty body, no results")
return { service: 'ytmusic' };
}
// I ignore the tabbedSearchResultsRenderer case from ytmusicapi, because we're always selecting a tab.
const results = body.contents.sectionListRenderer.contents
// no results
if (results.length == 1 && results.itemSectionRenderer !== undefined) {
debug("Only itemSectionRenderer, no results")
return { service: 'ytmusic' };
}
for (const result of results) {
if (result.musicShelfRenderer === undefined) {
continue;
}
const matches = parse_result_content(result.musicShelfRenderer.contents, data.type)
if (matches[0]) {
return await lookupId(matches[0], data.type)
}
}
debug("Finished looking up, no results")
return { service: 'ytmusic' };
}
function parse_result_content(contents, type) {
let matches = []
for (const result of contents) {
const data = result.musicResponsiveListItemRenderer;
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
} else if (type == "album") {
matchId = data.navigationEndpoint?.browseEndpoint.browseId
}
if(matchId) {
matches.push(matchId)
}
}
return matches
}
async function lookupTrack(id) {
let endpoint = "https://www.youtube.com/get_video_info"
const { body } = await request.get(endpoint).query({ video_id: id, hl: "en", el: "detailpage" })
@ -43,24 +139,17 @@ async function lookupTrack(id) {
}
async function lookupAlbum(id) {
let request_body = {'browseEndpointContextSupportedConfigs': {'browseEndpointContextMusicConfig': {'pageType': 'MUSIC_PAGE_TYPE_ALBUM'}}, 'browseId': id, '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}}}
let request_body = {'browseEndpointContextSupportedConfigs': {'browseEndpointContextMusicConfig': {'pageType': 'MUSIC_PAGE_TYPE_ALBUM'}}, 'browseId': id, ...standard_body }
let headers = {
"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",
"X-Goog-Visitor-Id": "CgtWaTB2WWRDeEFUYyjhv-X8BQ%3D%3D"
}
const { body } = await request.post("https://music.youtube.com/youtubei/v1/browse?alt=json")
.set(headers)
.query({ alt: "json", key: "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30"}) // INNERTUBE_API_KEY from music.youtube.com
const { body } = await request.post("https://music.youtube.com/youtubei/v1/browse")
.set(standard_headers)
.query(standard_params)
.send(request_body)
let data = body.frameworkUpdates.entityBatchUpdate.mutations
let data = body.frameworkUpdates?.entityBatchUpdate.mutations
if (data === undefined) {
throw new Error()
}
let album_data = data.find((entry) => {
if (entry.payload.musicAlbumRelease !== undefined) {
return true

View file

@ -13,6 +13,25 @@ describe('ytmusic', function(){
result.name.should.equal('One Vision (Remastered 2011)');
});
});
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 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 (){