From 8dcb1cbecc026294183d537db5b2f6037c8cf629 Mon Sep 17 00:00:00 2001 From: "Renato \"Lond\" Cerqueira" Date: Sun, 17 Jan 2021 02:03:23 +0100 Subject: [PATCH] Add search method and tests (WIP) Working for most cases, still having troubles with "various artists" --- lib/services/ytmusic/index.js | 119 +++++++++++++++++++++++++++++----- test/services/ytmusic.js | 19 ++++++ 2 files changed, 123 insertions(+), 15 deletions(-) diff --git a/lib/services/ytmusic/index.js b/lib/services/ytmusic/index.js index 0064400..0a0b100 100644 --- a/lib/services/ytmusic/index.js +++ b/lib/services/ytmusic/index.js @@ -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 diff --git a/test/services/ytmusic.js b/test/services/ytmusic.js index e91d834..5d27a4b 100644 --- a/test/services/ytmusic.js +++ b/test/services/ytmusic.js @@ -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 (){