const axios = require('axios'); class YTMusicScraper { constructor(options = {}) { this.baseUrl = 'https://music.youtube.com/youtubei/v1'; this.apiKey = ''; this.context = {}; this.debug = options.debug || false; this.delay = options.delay || 500; // ms antar request this.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36', 'Accept': '*/*', 'Accept-Language': 'id,en-US;q=0.9,en;q=0.8,pt;q=0.7,ar;q=0.6,ja;q=0.5', 'Content-Type': 'application/json', 'Origin': 'https://music.youtube.com', 'Referer': 'https://music.youtube.com/', 'sec-ch-ua': '"Chromium";v="154", "Google Chrome";v="154", "Not A(Brand";v="99"', 'sec-ch-ua-arch': '"x86"', 'sec-ch-ua-bitness': '"64"', 'sec-ch-ua-form-factors': '"Desktop"', 'sec-ch-ua-platform': '"Windows"', 'sec-ch-ua-platform-version': '"15.0.0"', 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-origin' }; } async init() { try { console.log('[*] Menginisialisasi scraper...'); const { data: html } = await axios.get('https://music.youtube.com/', { headers: this.headers }); const keyMatch = html.match(/"INNERTUBE_API_KEY":"([a-zA-Z0-9_-]+)"/); this.apiKey = keyMatch ? keyMatch[1] : 'AIzaSyC9XL3ZjWddXya6I7t8l2Qw5Yv8zX1z2Qw'; const ctxMatch = html.match(/"INNERTUBE_CONTEXT":(\{.+?\})\},\s*"INNERTUBE_CONTEXT_CLIENT_NAME"/); if (ctxMatch) { try { this.context = JSON.parse(ctxMatch[1]); } catch { this.context = this._defaultContext(); } } else { this.context = this._defaultContext(); } const visMatch = html.match(/"VISITOR_DATA":"([a-zA-Z0-9_-]+)"/); if (visMatch) { this.headers['X-Goog-Visitor-Id'] = visMatch[1]; if (this.context.client) this.context.client.visitorData = visMatch[1]; } console.log('[✓] Scraper siap!'); return this; } catch (err) { console.error('[!] Init gagal:', err.message); this.apiKey = 'AIzaSyC9XL3ZjWddXya6I7t8l2Qw5Yv8zX1z2Qw'; this.context = this._defaultContext(); return this; } } _defaultContext() { return { client: { clientName: 'WEB_REMIX', clientVersion: '1.20261006.01.00', hl: 'id', gl: 'ID', } }; } _sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async _request(endpoint, body = {}) { await this._sleep(this.delay); const url = `${this.baseUrl}/${endpoint}?prettyPrint=false`; const payload = { context: this.context.client ? { client: this.context.client } : this.context, ...body }; if (this.debug) { console.log(`\n[DEBUG] POST /${endpoint}`); console.log('[DEBUG] Payload:', JSON.stringify(payload).substring(0, 300)); } const { data } = await axios.post(url, payload, { headers: this.headers }); if (this.debug) { console.log('[DEBUG] Response keys:', Object.keys(data)); } return data; } _text(runs) { if (!runs) return ''; if (typeof runs === 'string') return runs; if (Array.isArray(runs)) return runs.map(r => r?.text || '').join(''); return runs.text || ''; } _thumbs(obj) { if (!obj) return []; if (obj.musicThumbnailRenderer?.thumbnail?.thumbnails) { return obj.musicThumbnailRenderer.thumbnail.thumbnails; } if (obj.musicItemThumbnailRenderer?.thumbnail?.thumbnails) { return obj.musicItemThumbnailRenderer.thumbnail.thumbnails; } if (obj.thumbnail?.thumbnails) return obj.thumbnail.thumbnails; if (obj.thumbnails) return obj.thumbnails; if (obj.thumbnailRenderer) return this._thumbs(obj.thumbnailRenderer); return []; } _bestThumb(obj) { const thumbs = this._thumbs(obj); if (thumbs.length === 0) return null; return thumbs[thumbs.length - 1].url; // yang terakhir biasanya paling besar } _findAll(obj, key, results = []) { if (!obj || typeof obj !== 'object') return results; if (obj[key] !== undefined) results.push(obj[key]); for (const val of Object.values(obj)) { if (val && typeof val === 'object') { this._findAll(val, key, results); } } return results; } _parseListItem(renderer) { if (!renderer) return null; try { const cols = renderer.flexColumns || []; const col0 = cols[0]?.musicResponsiveListItemFlexColumnRenderer; const col1 = cols[1]?.musicResponsiveListItemFlexColumnRenderer; const col2 = cols[2]?.musicResponsiveListItemFlexColumnRenderer; const title = this._text(col0?.text?.runs); if (!title) return null; let videoId = null; const playBtn = renderer.overlay ?.musicItemThumbnailOverlayRenderer ?.content ?.musicPlayButtonRenderer; if (playBtn?.playNavigationEndpoint?.watchEndpoint?.videoId) { videoId = playBtn.playNavigationEndpoint.watchEndpoint.videoId; } if (!videoId) { const runs0 = col0?.text?.runs || []; for (const run of runs0) { if (run.navigationEndpoint?.watchEndpoint?.videoId) { videoId = run.navigationEndpoint.watchEndpoint.videoId; break; } } } if (!videoId && renderer.playlistItemData?.videoId) { videoId = renderer.playlistItemData.videoId; } let type = ''; let artist = ''; let plays = ''; let artistId = null; const runs1 = col1?.text?.runs || []; for (const run of runs1) { const t = (run.text || '').trim(); if (!t || t === '•' || t === '·') continue; if (['Song', 'Lagu', 'Video', 'Album', 'Single', 'EP', 'Playlist', 'Artis', 'Artist', 'Podcast'].includes(t)) { type = t; continue; } if (/^[\d.,]+\s*(plays|views|listeners|jt|rb|ribu|M|K|k)/i.test(t)) { plays = t; continue; } if (run.navigationEndpoint?.browseEndpoint) { const pageType = run.navigationEndpoint.browseEndpoint ?.browseEndpointContextSupportedConfigs ?.browseEndpointContextMusicConfig ?.pageType || ''; if (pageType === 'MUSIC_PAGE_TYPE_ARTIST' || pageType === 'MUSIC_PAGE_TYPE_USER_CHANNEL') { artist = t; artistId = run.navigationEndpoint.browseEndpoint.browseId; } continue; } if (!artist && !type.includes('Artis')) { artist = t; } } let album = ''; let albumId = null; if (col2) { album = this._text(col2.text?.runs); const runs2 = col2.text?.runs || []; for (const run of runs2) { if (run.navigationEndpoint?.browseEndpoint?.browseId) { albumId = run.navigationEndpoint.browseEndpoint.browseId; break; } } } let browseId = null; let pageType = ''; if (renderer.navigationEndpoint?.browseEndpoint) { browseId = renderer.navigationEndpoint.browseEndpoint.browseId; pageType = renderer.navigationEndpoint.browseEndpoint ?.browseEndpointContextSupportedConfigs ?.browseEndpointContextMusicConfig ?.pageType || ''; } return { type: type || pageType || 'Unknown', videoId, browseId, title, artist, artistId, album, albumId, plays, thumbnail: this._bestThumb(renderer.thumbnail || renderer) }; } catch (err) { if (this.debug) console.error('[DEBUG] Parse error:', err.message); return null; } } _parseCard(card) { if (!card) return null; try { const title = this._text(card.title?.runs); const subtitle = this._text(card.subtitle?.runs); let videoId = null; let browseId = null; const buttons = card.buttons || []; for (const btn of buttons) { const playBtn = btn.musicPlayButtonRenderer || btn.buttonRenderer; if (playBtn?.playNavigationEndpoint?.watchEndpoint?.videoId) { videoId = playBtn.playNavigationEndpoint.watchEndpoint.videoId; } if (playBtn?.playNavigationEndpoint?.browseEndpoint?.browseId) { browseId = playBtn.playNavigationEndpoint.browseEndpoint.browseId; } } // Fallback dari navigationEndpoint langsung if (!browseId && card.onTap?.watchEndpoint?.videoId) { videoId = card.onTap.watchEndpoint.videoId; } if (!browseId && card.onTap?.browseEndpoint?.browseId) { browseId = card.onTap.browseEndpoint.browseId; } // Thumbnail let thumb = this._bestThumb(card.thumbnail); if (!thumb) { thumb = this._bestThumb(card.thumbnailRenderer); } return { type: 'Top Result', videoId, browseId, title, subtitle, thumbnail: thumb }; } catch (err) { if (this.debug) console.error('[DEBUG] Card parse error:', err.message); return null; } } _parseTwoRow(renderer) { if (!renderer) return null; try { const title = this._text(renderer.title?.runs); const subtitle = this._text(renderer.subtitle?.runs); let videoId = null; let browseId = null; let pageType = ''; const nav = renderer.navigationEndpoint; if (nav?.watchEndpoint?.videoId) videoId = nav.watchEndpoint.videoId; if (nav?.browseEndpoint?.browseId) { browseId = nav.browseEndpoint.browseId; pageType = nav.browseEndpoint ?.browseEndpointContextSupportedConfigs ?.browseEndpointContextMusicConfig ?.pageType || ''; } return { type: pageType || (videoId ? 'Song' : 'Browse'), videoId, browseId, title, subtitle, thumbnail: this._bestThumb(renderer.thumbnailRenderer || renderer.thumbnail) }; } catch { return null; } } async search(query, filter = '') { console.log(`\n[*] Mencari: "${query}"${filter ? ` (filter: ${filter})` : ''}...`); const body = { query }; const filters = { songs: 'EgWKAQIIAWoKEAkQBRAKEAMQBQ%3D%3D', videos: 'EgWKAQIQAWoKEAkQBRAKEAMQBQ%3D%3D', albums: 'EgWKAQIYAWoKEAkQBRAKEAMQBQ%3D%3D', artists: 'EgWKAQIgAWoKEAkQBRAKEAMQBQ%3D%3D', playlists: 'EgWKAQIoAWoKEAkQBRAKEAMQBQ%3D%3D' }; if (filter && filters[filter]) body.params = filters[filter]; const data = await this._request('search', body); const results = []; const tabs = data?.contents?.tabbedSearchResultsRenderer?.tabs || []; const sections = tabs[0]?.tabRenderer?.content?.sectionListRenderer?.contents || []; if (this.debug) { console.log(`[DEBUG] Sections found: ${sections.length}`); sections.forEach((s, i) => { console.log(`[DEBUG] [${i}] keys:`, Object.keys(s)); }); } for (const section of sections) { if (section.musicCardShelfRenderer) { const card = this._parseCard(section.musicCardShelfRenderer); if (card) results.push(card); } if (section.musicShelfRenderer) { const shelfTitle = this._text(section.musicShelfRenderer.title?.runs); const items = section.musicShelfRenderer.contents || []; if (this.debug) { console.log(`[DEBUG] Shelf "${shelfTitle}": ${items.length} items`); } for (const item of items) { const parsed = this._parseListItem(item.musicResponsiveListItemRenderer); if (parsed) { parsed.category = shelfTitle; results.push(parsed); } } } } if (results.length <= 1) { if (this.debug) console.log('[DEBUG] Fallback: recursive search...'); const allItems = this._findAll(data, 'musicResponsiveListItemRenderer'); for (const renderer of allItems) { const parsed = this._parseListItem(renderer); if (parsed) { const isDup = results.some(r => (r.videoId && r.videoId === parsed.videoId) || (r.browseId && r.browseId === parsed.browseId) || (r.title === parsed.title && r.artist === parsed.artist) ); if (!isDup) results.push(parsed); } } } if (results.length === 0) { console.log('[!] Tidak ada hasil. Aktifkan debug: true untuk lihat response.'); if (this.debug) { console.log('[DEBUG] Full response:', JSON.stringify(data, null, 2).substring(0, 2000)); } } return results; } async getSuggestions(query) { console.log(`\n[*] Suggestions untuk: "${query}"...`); const data = await this._request('music/get_search_suggestions', { input: query }); const suggestions = []; const sections = data?.contents || []; for (const section of sections) { const items = section?.searchSuggestionsSectionRenderer?.contents || []; for (const item of items) { if (item.searchSuggestionRenderer) { const text = this._text(item.searchSuggestionRenderer.suggestion?.runs); const navQuery = item.searchSuggestionRenderer ?.navigationEndpoint ?.searchEndpoint ?.query; suggestions.push({ type: 'suggestion', text, query: navQuery || text }); } if (item.musicResponsiveListItemRenderer) { const parsed = this._parseListItem(item.musicResponsiveListItemRenderer); if (parsed) { parsed.type = 'rich_suggestion'; suggestions.push(parsed); } } } } return suggestions; } async getHome() { console.log('\n[*] Mengambil Home...'); const data = await this._request('browse', { browseId: 'FEmusic_home' }); return this._parseBrowseSections(data); } async getExplore() { console.log('\n[*] Mengambil Explore...'); const data = await this._request('browse', { browseId: 'FEmusic_explore' }); return this._parseBrowseSections(data); } async getCharts() { console.log('\n[*] Mengambil Charts...'); const data = await this._request('browse', { browseId: 'FEmusic_charts' }); return this._parseBrowseSections(data); } _parseBrowseSections(data) { const sections = []; const tabs = data?.contents?.singleColumnBrowseResultsRenderer?.tabs || []; const contents = tabs[0]?.tabRenderer?.content?.sectionListRenderer?.contents || []; for (const section of contents) { const shelf = section.musicCarouselShelfRenderer || section.musicImmersiveCarouselShelfRenderer || section.musicShelfRenderer; if (shelf) { const header = this._text( shelf.header?.musicCarouselShelfBasicHeaderRenderer?.title?.runs || shelf.header?.musicShelfHeaderRenderer?.title?.runs || shelf.title?.runs ) || 'Section'; const items = (shelf.contents || []).map(c => { if (c.musicTwoRowItemRenderer) { return this._parseTwoRow(c.musicTwoRowItemRenderer); } if (c.musicResponsiveListItemRenderer) { return this._parseListItem(c.musicResponsiveListItemRenderer); } if (c.musicNavigationButtonRenderer) { const btn = c.musicNavigationButtonRenderer; return { type: 'shortcut', title: this._text(btn.buttonText?.runs), browseId: btn.clickCommand?.browseEndpoint?.browseId || null }; } return null; }).filter(Boolean); sections.push({ header, items }); } } return sections; } async getDetail(videoId) { console.log(`\n[*] Detail untuk: ${videoId}...`); const data = await this._request('next', { videoId, isAudioOnly: true }); const detail = { videoId, title: '', artist: '', album: '', duration: '', lyricsBrowseId: null, relatedBrowseId: null, queue: [] }; try { const tabs = data?.contents ?.singleColumnMusicWatchNextResultsRenderer ?.tabbedRenderer ?.watchNextTabbedResultsRenderer ?.tabs || []; if (tabs[0]) { const queueRenderer = tabs[0].tabRenderer?.content ?.musicQueueRenderer?.content ?.playlistPanelRenderer; if (queueRenderer?.contents) { detail.queue = queueRenderer.contents.map(item => { const r = item.playlistPanelVideoRenderer; if (!r) return null; return { videoId: r.videoId, title: this._text(r.title?.runs), artist: this._text(r.longBylineText?.runs || r.shortBylineText?.runs), duration: this._text(r.lengthText?.runs), thumbnail: this._bestThumb(r.thumbnail) }; }).filter(Boolean); if (detail.queue.length > 0) { detail.title = detail.queue[0].title; detail.artist = detail.queue[0].artist; detail.duration = detail.queue[0].duration; } } } if (tabs[1]?.tabRenderer?.endpoint?.browseEndpoint?.browseId) { detail.lyricsBrowseId = tabs[1].tabRenderer.endpoint.browseEndpoint.browseId; } if (tabs[2]?.tabRenderer?.endpoint?.browseEndpoint?.browseId) { detail.relatedBrowseId = tabs[2].tabRenderer.endpoint.browseEndpoint.browseId; } } catch (err) { console.error('[!] Error parsing detail:', err.message); } return detail; } async getLyrics(browseId) { if (!browseId) return null; console.log('\n[*] Mengambil lirik...'); const data = await this._request('browse', { browseId }); try { const contents = data?.contents?.sectionListRenderer?.contents || []; for (const section of contents) { const desc = section?.musicDescriptionShelfRenderer; if (desc) { return { source: this._text(desc.header?.runs), lyrics: this._text(desc.description?.runs), footer: this._text(desc.footer?.runs) }; } } } catch (err) { console.error('[!] Error parsing lyrics:', err.message); } return null; } async getAlbum(browseId) { console.log(`\n[*] Mengambil album: ${browseId}...`); const data = await this._request('browse', { browseId }); try { const header = data?.header?.musicImmersiveHeaderRenderer || data?.header?.musicDetailHeaderRenderer || data?.header?.musicVisualHeaderRenderer; const album = { browseId, title: this._text(header?.title?.runs), artist: this._text(header?.subtitle?.runs), year: '', tracks: [] }; const contents = data?.contents?.singleColumnBrowseResultsRenderer ?.tabs?.[0]?.tabRenderer?.content ?.sectionListRenderer?.contents || []; for (const section of contents) { const shelf = section?.musicShelfRenderer; if (shelf?.contents) { for (const item of shelf.contents) { const r = item?.musicResponsiveListItemRenderer; if (r) { const parsed = this._parseListItem(r); if (parsed) album.tracks.push(parsed); } } } } // Kalau kosong, coba recursive if (album.tracks.length === 0) { const allItems = this._findAll(data, 'musicResponsiveListItemRenderer'); album.tracks = allItems .map(r => this._parseListItem(r)) .filter(Boolean); } return album; } catch (err) { console.error('[!] Error parsing album:', err.message); return null; } } async getArtist(browseId) { console.log(`\n[*] Mengambil artis: ${browseId}...`); const data = await this._request('browse', { browseId }); try { const header = data?.header?.musicImmersiveHeaderRenderer; const artist = { browseId, name: this._text(header?.title?.runs), description: this._text(header?.description?.runs), subscribers: this._text(header?.subscriptionButton?.subscribeButtonRenderer?.subscriberCountText?.runs), sections: [] }; const contents = data?.contents?.singleColumnBrowseResultsRenderer ?.tabs?.[0]?.tabRenderer?.content ?.sectionListRenderer?.contents || []; for (const section of contents) { const shelf = section?.musicShelfRenderer || section?.musicCarouselShelfRenderer; if (shelf) { const title = this._text( shelf.title?.runs || shelf.header?.musicCarouselShelfBasicHeaderRenderer?.title?.runs ) || 'Section'; const items = (shelf.contents || []).map(c => { if (c.musicResponsiveListItemRenderer) { return this._parseListItem(c.musicResponsiveListItemRenderer); } if (c.musicTwoRowItemRenderer) { return this._parseTwoRow(c.musicTwoRowItemRenderer); } return null; }).filter(Boolean); artist.sections.push({ title, items }); } } return artist; } catch (err) { console.error('[!] Error parsing artist:', err.message); return null; } } async getPlaylist(browseId) { console.log(`\n[*] Mengambil playlist: ${browseId}...`); const data = await this._request('browse', { browseId }); try { const header = data?.header?.musicDetailHeaderRenderer; const playlist = { browseId, title: this._text(header?.title?.runs), description: this._text(header?.description?.runs), author: this._text(header?.subtitle?.runs), tracks: [] }; const contents = data?.contents?.singleColumnBrowseResultsRenderer ?.tabs?.[0]?.tabRenderer?.content ?.sectionListRenderer?.contents || []; for (const section of contents) { const shelf = section?.musicPlaylistShelfRenderer || section?.musicShelfRenderer; if (shelf?.contents) { for (const item of shelf.contents) { const r = item?.musicResponsiveListItemRenderer; if (r) { const parsed = this._parseListItem(r); if (parsed) playlist.tracks.push(parsed); } } } } if (playlist.tracks.length === 0) { const allItems = this._findAll(data, 'musicResponsiveListItemRenderer'); playlist.tracks = allItems .map(r => this._parseListItem(r)) .filter(Boolean); } return playlist; } catch (err) { console.error('[!] Error parsing playlist:', err.message); return null; } } } async function main() { const ytm = new YTMusicScraper({ debug: false, delay: 300 }); await ytm.init(); const searchResults = await ytm.search('Judika'); console.log('\n========== HASIL SEARCH =========='); console.log(JSON.stringify(searchResults, null, 2)); const songResults = await ytm.search('januari glenn fredly', 'songs'); console.log('\n========== SEARCH SONGS =========='); console.log(JSON.stringify(songResults, null, 2)); const suggestions = await ytm.getSuggestions('januari glenn'); console.log('\n========== SUGGESTIONS =========='); console.log(JSON.stringify(suggestions, null, 2)); const home = await ytm.getHome(); console.log('\n========== HOME =========='); console.log(JSON.stringify(home, null, 2)); const explore = await ytm.getExplore(); console.log('\n========== EXPLORE =========='); console.log(JSON.stringify(explore, null, 2)); const song = songResults.find(r => r.videoId) || searchResults.find(r => r.videoId); if (song?.videoId) { const detail = await ytm.getDetail(song.videoId); console.log('\n========== DETAIL =========='); console.log(JSON.stringify(detail, null, 2)); if (detail.lyricsBrowseId) { const lyrics = await ytm.getLyrics(detail.lyricsBrowseId); console.log('\n========== LYRICS =========='); console.log(JSON.stringify(lyrics, null, 2)); } } const withAlbum = searchResults.find(r => r.albumId); if (withAlbum?.albumId) { const album = await ytm.getAlbum(withAlbum.albumId); console.log('\n========== ALBUM =========='); console.log(JSON.stringify(album, null, 2)); } const withArtist = searchResults.find(r => r.artistId); if (withArtist?.artistId) { const artist = await ytm.getArtist(withArtist.artistId); console.log('\n========== ARTIST =========='); console.log(JSON.stringify(artist, null, 2)); } } main().catch(err => { console.error('\n[FATAL]', err.message); if (err.response) { console.error('Status:', err.response.status); console.error('Data:', JSON.stringify(err.response.data).substring(0, 500)); } });