|
import { PassThrough } from 'node:stream' |
|
|
|
import config from '../../config.js' |
|
import constants from '../../constants.js' |
|
import { debugLog, makeRequest, encodeTrack, randomLetters, loadHLSPlaylist } from '../utils.js' |
|
|
|
const ytContext = { |
|
...(config.options.bypassAgeRestriction ? { |
|
thirdParty: { |
|
embedUrl: 'https://www.youtube.com' |
|
}, |
|
} : {}), |
|
client: { |
|
...(!config.options.bypassAgeRestriction ? { |
|
userAgent: 'com.google.android.youtube/19.13.34 (Linux; U; Android 14 gzip)', |
|
clientName: 'ANDROID', |
|
clientVersion: '19.13.34', |
|
} : { |
|
clientName: 'TVHTML5_SIMPLY_EMBEDDED_PLAYER', |
|
clientVersion: '2.0', |
|
userAgent: 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0' |
|
}), |
|
screenDensityFloat: 1, |
|
screenHeightPoints: 1080, |
|
screenPixelDensity: 1, |
|
screenWidthPoints: 1920, |
|
} |
|
} |
|
|
|
const sourceInfo = { |
|
innertubeInterval: null, |
|
signatureTimestamp: null, |
|
functions: [] |
|
} |
|
|
|
function _getBaseHostRequest(type) { |
|
if (ytContext.client.clientName.startsWith('ANDROID')) |
|
return 'youtubei.googleapis.com' |
|
|
|
return `${type === 'ytmusic' ? 'music' : 'www'}.youtube.com` |
|
} |
|
|
|
function _getBaseHost(type) { |
|
return `${type === 'ytmusic' ? 'music' : 'www'}.youtube.com` |
|
} |
|
|
|
function _switchClient(newClient) { |
|
if (newClient === 'ANDROID') { |
|
ytContext.client.clientName = 'ANDROID' |
|
ytContext.client.clientVersion = '19.04.33' |
|
ytContext.client.userAgent = 'com.google.android.youtube/19.04.33 (Linux; U; Android 14 gzip)' |
|
} else if (newClient === 'ANDROID_MUSIC') { |
|
ytContext.client.clientName = 'ANDROID_MUSIC' |
|
ytContext.client.clientVersion = '6.37.50' |
|
ytContext.client.userAgent = 'com.google.android.apps.youtube.music/6.37.50 (Linux; U; Android 14 gzip)' |
|
} |
|
} |
|
|
|
function _getSourceName(type) { |
|
return type === 'ytmusic' ? 'YouTube Music' : 'YouTube' |
|
} |
|
|
|
async function _init() { |
|
debugLog('youtube', 5, { type: 1, message: 'Fetching deciphering functions...' }) |
|
|
|
const { body: data } = await makeRequest('https://www.youtube.com/embed', { method: 'GET' }).catch((err) => { |
|
debugLog('youtube', 5, { type: 2, message: `Failed to access YouTube website: ${err.message}` }) |
|
}) |
|
|
|
const { body: player } = await makeRequest(`https://www.youtube.com${/(?<=jsUrl":")[^"]+/.exec(data)[0]}`, { method: 'GET' }).catch((err) => { |
|
debugLog('youtube', 5, { type: 2, message: `Failed to fetch player.js: ${err.message}` }) |
|
}) |
|
|
|
sourceInfo.signatureTimestamp = /(?<=signatureTimestamp:)[0-9]+/.exec(player)[0] |
|
|
|
let functionName = player.match(/a.set\("alr","yes"\);c&&\(c=(.*?)\(/)[1] |
|
const decipherFunctionName = functionName |
|
|
|
const sigFunction = player.match(new RegExp(`${functionName.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}=function\\(a\\){(.*)\\)};`, 'g'))[0] |
|
|
|
functionName = player.match(/a=a\.split\(""\);(.*?)\./)[1] |
|
const sigWrapper = player.match(new RegExp(`var ${functionName}={(.*?)};`, 's'))[1] |
|
|
|
sourceInfo.functions[0] = `const ${functionName}={${sigWrapper}};const ${sigFunction}${decipherFunctionName}(sig);` |
|
|
|
functionName = player.match(/&&\(b=a\.get\("n"\)\)&&\(b=(.*?)\(/)[1] |
|
|
|
if (functionName && functionName.includes('[')) |
|
functionName = player.match(new RegExp(`${functionName.match(/([^[]*)\[/)[1]}=\\[(.*?)]`))[1] |
|
|
|
const ncodeFunction = player.match(new RegExp(`${functionName}=function(.*?)};`, 's'))[1] |
|
sourceInfo.functions[1] = `const ${functionName} = function${ncodeFunction}};${functionName}(ncode)` |
|
|
|
debugLog('youtube', 5, { type: 1, message: 'Successfully fetched deciphering functions.' }) |
|
} |
|
|
|
async function init() { |
|
debugLog('youtube', 5, { type: 1, message: 'Unrecommended option "bypass age-restricted" is enabled.' }) |
|
|
|
await _init() |
|
|
|
sourceInfo.innertubeInterval = setInterval(async () => _init(), 3600000) |
|
} |
|
|
|
function free() { |
|
clearInterval(sourceInfo.innertubeInterval) |
|
sourceInfo.innertubeInterval = null |
|
|
|
sourceInfo.signatureTimestamp = null |
|
sourceInfo.functions = [] |
|
} |
|
|
|
function checkURLType(url, type) { |
|
if (type === 'ytmusic') { |
|
const videoRegex = /^https?:\/\/music\.youtube\.com\/watch\?v=[\w-]+/ |
|
const playlistRegex = /^https?:\/\/music\.youtube\.com\/playlist\?list=[\w-]+/ |
|
const selectedVideoRegex = /^https?:\/\/music\.youtube\.com\/watch\?v=[\w-]+&list=[\w-]+/ |
|
|
|
if (selectedVideoRegex.test(url) || playlistRegex.test(url)) return constants.YouTube.playlist |
|
else if (videoRegex.test(url)) return constants.YouTube.video |
|
else return -1 |
|
} else { |
|
const videoRegex = /^https?:\/\/(?:www\.)?(?:youtube\.com\/(?:watch\?v=[\w-]+)|youtu\.be\/[\w-]+)/ |
|
const playlistRegex = /^https?:\/\/(?:www\.)?youtube\.com\/playlist\?list=[\w-]+/ |
|
const selectedVideoRegex = /^https?:\/\/(?:www\.)?youtube\.com\/watch\?v=[\w-]+&list=[\w-]+/ |
|
const shortsRegex = /^https?:\/\/(?:www\.)?youtube\.com\/shorts\/[\w-]+/ |
|
|
|
if (selectedVideoRegex.test(url) || playlistRegex.test(url)) return constants.YouTube.playlist |
|
else if (shortsRegex.test(url)) return constants.YouTube.shorts |
|
else if (videoRegex.test(url)) return constants.YouTube.video |
|
else return -1 |
|
} |
|
} |
|
|
|
async function search(query, type, shouldLog) { |
|
if (shouldLog) debugLog('search', 4, { type: 1, sourceName: _getSourceName(type), query }) |
|
|
|
if (!config.options.bypassAgeRestriction) |
|
_switchClient(type === 'ytmusic' ? 'ANDROID_MUSIC' : 'ANDROID') |
|
|
|
const { body: search } = await makeRequest(`https://${_getBaseHostRequest(type)}/youtubei/v1/search`, { |
|
headers: { |
|
'User-Agent': ytContext.client.userAgent, |
|
...(config.search.sources.youtube.authentication.enabled ? { |
|
Authorization: config.search.sources.youtube.authentication.authorization, |
|
Cookie: `SID=${config.search.sources.youtube.authentication.cookies.SID}; LOGIN_INFO=${config.search.sources.youtube.authentication.cookies.LOGIN_INFO}` |
|
} : {}) |
|
}, |
|
body: { |
|
context: ytContext, |
|
query, |
|
params: type === 'ytmusic' ? 'EgWKAQIIAWoQEAMQBBAJEAoQBRAREBAQFQ%3D%3D' : 'EgIQAQ%3D%3D' |
|
}, |
|
method: 'POST', |
|
disableBodyCompression: true |
|
}) |
|
|
|
if (typeof search !== 'object') { |
|
debugLog('search', 4, { type: 3, sourceName: _getSourceName(type), query, message: 'Failed to load results.' }) |
|
|
|
return { |
|
loadType: 'error', |
|
data: { |
|
message: 'Failed to load results.', |
|
severity: 'common', |
|
cause: 'Unknown' |
|
} |
|
} |
|
} |
|
|
|
if (search.error) { |
|
debugLog('search', 4, { type: 3, sourceName: _getSourceName(type), query, message: search.error.message }) |
|
|
|
return { |
|
loadType: 'error', |
|
data: { |
|
message: search.error.message, |
|
severity: 'fault', |
|
cause: 'Unknown' |
|
} |
|
} |
|
} |
|
|
|
const tracks = [] |
|
|
|
let videos = null |
|
if (config.options.bypassAgeRestriction) videos = type == 'ytmusic' ? search.contents.sectionListRenderer.contents[0].itemSectionRenderer.contents : search.contents.sectionListRenderer.contents[search.contents.sectionListRenderer.contents.length - 1].itemSectionRenderer.contents |
|
else videos = type == 'ytmusic' ? search.contents.tabbedSearchResultsRenderer.tabs[0].tabRenderer.content.musicSplitViewRenderer.mainContent.sectionListRenderer.contents[0].musicShelfRenderer.contents : search.contents.sectionListRenderer.contents[search.contents.sectionListRenderer.contents.length - 1].itemSectionRenderer.contents |
|
|
|
if (videos.length > config.options.maxSearchResults) |
|
videos = videos.slice(0, config.options.maxSearchResults) |
|
|
|
videos.forEach((video) => { |
|
video = video.compactVideoRenderer || video.musicTwoColumnItemRenderer |
|
|
|
if (video) { |
|
const identifier = type === 'ytmusic' ? video.navigationEndpoint.watchEndpoint.videoId : video.videoId |
|
const length = type === 'ytmusic' && !config.options.bypassAgeRestriction ? video.subtitle.runs[2].text : video.lengthText?.runs[0]?.text |
|
const thumbnails = type === 'ytmusic' && !config.options.bypassAgeRestriction ? video.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails : video.thumbnail.thumbnails |
|
|
|
const track = { |
|
identifier, |
|
isSeekable: true, |
|
author: video.longBylineText ? video.longBylineText.runs[0].text : video.subtitle.runs[0].text, |
|
length: length ? (parseInt(length.split(':')[0]) * 60 + parseInt(length.split(':')[1])) * 1000 : 0, |
|
isStream: !length, |
|
position: 0, |
|
title: video.title.runs[0].text, |
|
uri: `https://${_getBaseHost(type)}/watch?v=${identifier}`, |
|
artworkUrl: thumbnails[thumbnails.length - 1].url.split('?')[0], |
|
isrc: null, |
|
sourceName: type |
|
} |
|
|
|
tracks.push({ |
|
encoded: encodeTrack(track), |
|
info: track, |
|
pluginInfo: {} |
|
}) |
|
} |
|
}) |
|
|
|
if (tracks.length === 0) { |
|
debugLog('search', 4, { type: 3, sourceName: _getSourceName(type), query, message: 'No matches found.' }) |
|
|
|
return { |
|
loadType: 'empty', |
|
data: {} |
|
} |
|
} |
|
|
|
if (shouldLog) |
|
debugLog('search', 4, { type: 2, sourceName: _getSourceName(type), tracksLen: tracks.length, query }) |
|
|
|
return { |
|
loadType: 'search', |
|
data: tracks |
|
} |
|
} |
|
|
|
async function loadFrom(query, type) { |
|
if (!config.options.bypassAgeRestriction) |
|
_switchClient(type === 'ytmusic' ? 'ANDROID_MUSIC' : 'ANDROID') |
|
|
|
switch (checkURLType(query, type)) { |
|
case constants.YouTube.video: { |
|
debugLog('loadtracks', 4, { type: 1, loadType: 'track', sourceName: _getSourceName(type), query }) |
|
|
|
const identifier = (/v=([^&]+)/.exec(query) || /youtu\.be\/([^?]+)/.exec(query))[1] |
|
|
|
const { body: video } = await makeRequest(`https://${_getBaseHostRequest(type)}/youtubei/v1/player?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8&prettyPrint=false`, { |
|
headers: { |
|
'User-Agent': ytContext.client.userAgent, |
|
...(config.search.sources.youtube.authentication.enabled ? { |
|
Authorization: config.search.sources.youtube.authentication.authorization, |
|
Cookie: `SID=${config.search.sources.youtube.authentication.cookies.SID}; LOGIN_INFO=${config.search.sources.youtube.authentication.cookies.LOGIN_INFO}` |
|
} : {}) |
|
}, |
|
body: { |
|
context: ytContext, |
|
videoId: identifier, |
|
contentCheckOk: true, |
|
racyCheckOk: true, |
|
params: 'CgIQBg' |
|
}, |
|
method: 'POST' |
|
}) |
|
|
|
if (video.playabilityStatus.status !== 'OK') { |
|
const errorMessage = video.playabilityStatus.reason || video.playabilityStatus.messages[0] |
|
|
|
debugLog('loadtracks', 4, { type: 3, loadType: 'track', sourceName: _getSourceName(type), query, message: errorMessage }) |
|
|
|
return { |
|
loadType: 'error', |
|
data: { |
|
message: errorMessage, |
|
severity: 'common', |
|
cause: 'Unknown' |
|
} |
|
} |
|
} |
|
|
|
const track = { |
|
identifier: video.videoDetails.videoId, |
|
isSeekable: true, |
|
author: video.videoDetails.author, |
|
length: parseInt(video.videoDetails.lengthSeconds) * 1000, |
|
isStream: video.videoDetails.isLiveContent, |
|
position: 0, |
|
title: video.videoDetails.title, |
|
uri: `https://${_getBaseHost(type)}/watch?v=${video.videoDetails.videoId}`, |
|
artworkUrl: video.videoDetails.thumbnail.thumbnails[video.videoDetails.thumbnail.thumbnails.length - 1].url, |
|
isrc: null, |
|
sourceName: type |
|
} |
|
|
|
debugLog('loadtracks', 4, { type: 2, loadType: 'track', sourceName: _getSourceName(type), track, query }) |
|
|
|
return { |
|
loadType: 'track', |
|
data: { |
|
encoded: encodeTrack(track), |
|
info: track, |
|
pluginInfo: {} |
|
} |
|
} |
|
} |
|
case constants.YouTube.playlist: { |
|
debugLog('loadtracks', 4, { type: 1, loadType: 'playlist', sourceName: _getSourceName(type), query }) |
|
|
|
let identifier = /v=([^&]+)/.exec(query) |
|
if (identifier) identifier = identifier[1] |
|
|
|
const { body: playlist } = await makeRequest(`https://${_getBaseHostRequest(type)}/youtubei/v1/next?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8&prettyPrint=false`, { |
|
headers: { |
|
'User-Agent': ytContext.client.userAgent, |
|
...(config.search.sources.youtube.authentication.enabled ? { |
|
Authorization: config.search.sources.youtube.authentication.authorization, |
|
Cookie: `SID=${config.search.sources.youtube.authentication.cookies.SID}; LOGIN_INFO=${config.search.sources.youtube.authentication.cookies.LOGIN_INFO}` |
|
} : {}) |
|
}, |
|
body: { |
|
context: ytContext, |
|
playlistId: /(?<=list=)[\w-]+/.exec(query)[0], |
|
contentCheckOk: true, |
|
racyCheckOk: true, |
|
params: 'CgIQBg' |
|
}, |
|
method: 'POST' |
|
}) |
|
|
|
let contentsRoot = null |
|
|
|
if (config.options.bypassAgeRestriction) contentsRoot = playlist.contents.singleColumnWatchNextResults.playlist |
|
else contentsRoot = type === 'ytmusic' ? playlist.contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer.watchNextTabbedResultsRenderer.tabs[0].tabRenderer.content.musicQueueRenderer : playlist.contents.singleColumnWatchNextResults |
|
|
|
if (!(type === 'ytmusic' && !config.options.bypassAgeRestriction ? contentsRoot.content : contentsRoot)) { |
|
debugLog('loadtracks', 4, { type: 3, loadType: 'playlist', sourceName: _getSourceName(type), query, message: 'No matches found.' }) |
|
|
|
return { |
|
loadType: 'empty', |
|
data: {} |
|
} |
|
} |
|
|
|
const tracks = [] |
|
let selectedTrack = 0 |
|
|
|
let playlistContent = null |
|
|
|
if (config.options.bypassAgeRestriction) playlistContent = contentsRoot.playlist.contents |
|
else playlistContent = type === 'ytmusic' ? contentsRoot.content.playlistPanelRenderer.contents : contentsRoot.playlist?.playlist?.contents |
|
|
|
if (!playlistContent) { |
|
debugLog('loadtracks', 4, { type: 3, loadType: 'playlist', sourceName: _getSourceName(type), query, message: 'No matches found.' }) |
|
|
|
return { |
|
loadType: 'empty', |
|
data: {} |
|
} |
|
} |
|
|
|
if (playlistContent.length > config.options.maxAlbumPlaylistLength) |
|
playlistContent = playlistContent.slice(0, config.options.maxAlbumPlaylistLength) |
|
|
|
playlistContent.forEach((video, i) => { |
|
video = video.playlistPanelVideoRenderer || video.gridVideoRenderer |
|
|
|
if (video) { |
|
const track = { |
|
identifier: video.videoId, |
|
isSeekable: true, |
|
author: video.shortBylineText.runs ? video.shortBylineText.runs[0].text : 'Unknown author', |
|
length: video.lengthText ? (parseInt(video.lengthText.runs[0].text.split(':')[0]) * 60 + parseInt(video.lengthText.runs[0].text.split(':')[1])) * 1000 : 0, |
|
isStream: false, |
|
position: 0, |
|
title: video.title.runs[0].text, |
|
uri: `https://${_getBaseHost(type)}/watch?v=${video.videoId}`, |
|
artworkUrl: video.thumbnail.thumbnails[video.thumbnail.thumbnails.length - 1].url, |
|
isrc: null, |
|
sourceName: 'youtube' |
|
} |
|
|
|
tracks.push({ |
|
encoded: encodeTrack(track), |
|
info: track, |
|
pluginInfo: {} |
|
}) |
|
|
|
if (identifier && track.identifier === identifier) |
|
selectedTrack = i |
|
} |
|
}) |
|
|
|
if (tracks.length === 0) { |
|
debugLog('loadtracks', 4, { type: 3, loadType: 'playlist', sourceName: _getSourceName(type), query, message: 'No matches found.' }) |
|
|
|
return { |
|
loadType: 'empty', |
|
data: {} |
|
} |
|
} |
|
|
|
let playlistName = null |
|
|
|
if (config.options.bypassAgeRestriction) playlistName = contentsRoot.playlist.title |
|
else playlistName = type === 'ytmusic' ? contentsRoot.header.musicQueueHeaderRenderer.subtitle.runs[0].text : contentsRoot.playlist.playlist.title |
|
|
|
debugLog('loadtracks', 4, { type: 2, loadType: 'playlist', sourceName: _getSourceName(type), playlistName: playlistName }) |
|
|
|
return { |
|
loadType: 'playlist', |
|
data: { |
|
info: { |
|
name: playlistName, |
|
selectedTrack: selectedTrack |
|
}, |
|
pluginInfo: {}, |
|
tracks |
|
} |
|
} |
|
} |
|
case constants.YouTube.shorts: { |
|
debugLog('loadtracks', 4, { type: 1, loadType: 'track', sourceName: 'YouTube Shorts', query }) |
|
|
|
const { body: short } = await makeRequest(`https://${_getBaseHostRequest(type)}/youtubei/v1/player?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8&prettyPrint=false`, { |
|
headers: { |
|
'User-Agent': ytContext.client.userAgent, |
|
...(config.search.sources.youtube.authentication.enabled ? { |
|
Authorization: config.search.sources.youtube.authentication.authorization, |
|
Cookie: `SID=${config.search.sources.youtube.authentication.cookies.SID}; LOGIN_INFO=${config.search.sources.youtube.authentication.cookies.LOGIN_INFO}` |
|
} : {}) |
|
}, |
|
body: { |
|
context: ytContext, |
|
videoId: /shorts\/([a-zA-Z0-9_-]+)/.exec(query)[1], |
|
contentCheckOk: true, |
|
racyCheckOk: true, |
|
params: 'CgIQBg' |
|
}, |
|
method: 'POST' |
|
}) |
|
|
|
if (short.playabilityStatus.status !== 'OK') { |
|
const errorMessage = short.playabilityStatus.reason || short.playabilityStatus.messages[0] |
|
|
|
debugLog('loadtracks', 4, { type: 3, loadType: 'track', sourceName: 'YouTube Shorts', query, message: errorMessage }) |
|
|
|
return { |
|
loadType: 'error', |
|
data: { message: errorMessage, |
|
severity: 'common', |
|
cause: 'Unknown' |
|
} |
|
} |
|
} |
|
|
|
const track = { |
|
identifier: short.videoDetails.videoId, |
|
isSeekable: true, |
|
author: short.videoDetails.author, |
|
length: parseInt(short.videoDetails.lengthSeconds) * 1000, |
|
isStream: false, |
|
position: 0, |
|
title: short.videoDetails.title, |
|
uri: `https://${_getBaseHost(type)}/watch?v=${short.videoDetails.videoId}`, |
|
artworkUrl: short.videoDetails.thumbnail.thumbnails[short.videoDetails.thumbnail.thumbnails.length - 1].url, |
|
isrc: null, |
|
sourceName: 'youtube' |
|
} |
|
|
|
debugLog('loadtracks', 4, { type: 2, loadType: 'track', sourceName: 'YouTube Shorts', track, query }) |
|
|
|
return { |
|
loadType: 'short', |
|
data: { |
|
encoded: encodeTrack(track), |
|
info: track, |
|
pluginInfo: {} |
|
} |
|
} |
|
} |
|
default: { |
|
debugLog('loadtracks', 4, { type: 3, loadType: 'unknown', sourceName: _getSourceName(type), query, message: 'No matches found.' }) |
|
|
|
return { |
|
loadType: 'empty', |
|
data: {} |
|
} |
|
} |
|
} |
|
} |
|
|
|
async function retrieveStream(identifier, type, title) { |
|
if (!config.options.bypassAgeRestriction) |
|
_switchClient(type === 'ytmusic' ? 'ANDROID_MUSIC' : 'ANDROID') |
|
|
|
const { body: videos } = await makeRequest(`https://${_getBaseHostRequest(type)}/youtubei/v1/player?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8&prettyPrint=false&t=${randomLetters(12)}&id=${identifier}`, { |
|
headers: { |
|
'User-Agent': ytContext.client.userAgent, |
|
...(config.search.sources.youtube.authentication.enabled ? { |
|
Authorization: config.search.sources.youtube.authentication.authorization, |
|
Cookie: `SID=${config.search.sources.youtube.authentication.cookies.SID}; LOGIN_INFO=${config.search.sources.youtube.authentication.cookies.LOGIN_INFO}` |
|
} : {}) |
|
}, |
|
body: { |
|
context: ytContext, |
|
cpn: randomLetters(16), |
|
...(config.options.bypassAgeRestriction ? { |
|
playbackContext: { |
|
contentPlaybackContext: { |
|
signatureTimestamp: sourceInfo.signatureTimestamp |
|
} |
|
} |
|
} : {}), |
|
videoId: identifier, |
|
contentCheckOk: true, |
|
racyCheckOk: true, |
|
params: 'CgIQBg' |
|
}, |
|
method: 'POST', |
|
disableBodyCompression: true |
|
}) |
|
|
|
if (videos.playabilityStatus.status !== 'OK') { |
|
debugLog('retrieveStream', 4, { type: 2, sourceName: _getSourceName(type), query: title, message: videos.playabilityStatus.reason }) |
|
|
|
return { |
|
exception: { |
|
message: videos.playabilityStatus.reason, |
|
severity: 'common', |
|
cause: 'Unknown' |
|
} |
|
} |
|
} |
|
|
|
let itag = null |
|
switch (config.audio.quality) { |
|
case 'high': itag = 251; break |
|
case 'medium': itag = 250; break |
|
case 'low': itag = 249; break |
|
case 'lowest': itag = 599; break |
|
default: itag = 251; break |
|
} |
|
|
|
const audio = videos.streamingData.adaptiveFormats.find((format) => format.itag === itag) || videos.streamingData.adaptiveFormats.find((format) => format.mimeType.startsWith('audio/')) |
|
let url = audio.url || audio.signatureCipher || audio.cipher |
|
|
|
if ((audio.signatureCipher || audio.cipher) && config.options.bypassAgeRestriction) { |
|
const args = new URLSearchParams(url) |
|
url = decodeURIComponent(args.get('url')) |
|
|
|
if (audio.signatureCipher || audio.cipher) |
|
url += `&${args.get('sp')}=${eval(`const sig = "${args.get('s')}";` + sourceInfo.functions[0])}` |
|
} else { |
|
url = decodeURIComponent(url) |
|
} |
|
|
|
url += `&rn=1&cpn=${randomLetters(16)}&ratebypass=yes&range=0-` /* range query is necessary to bypass throttling */ |
|
|
|
return { |
|
url: videos.streamingData.hlsManifestUrl ? videos.streamingData.hlsManifestUrl : url, |
|
protocol: videos.streamingData.hlsManifestUrl ? 'hls' : 'http', |
|
format: audio.mimeType === 'audio/webm; codecs="opus"' ? 'webm/opus' : 'arbitrary' |
|
} |
|
} |
|
|
|
function loadLyrics(decodedTrack, language) { |
|
return new Promise(async (resolve) => { |
|
if (!config.options.bypassAgeRestriction) |
|
_switchClient(decodedTrack.sourceName === 'ytmusic' ? 'ANDROID_MUSIC' : 'ANDROID') |
|
|
|
const { body: video } = await makeRequest(`https://${_getBaseHostRequest(decodedTrack.sourceName)}/youtubei/v1/player?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8&prettyPrint=false`, { |
|
headers: { |
|
'User-Agent': ytContext.client.userAgent, |
|
...(config.search.sources.youtube.authentication.enabled ? { |
|
Authorization: config.search.sources.youtube.authentication.authorization, |
|
Cookie: `SID=${config.search.sources.youtube.authentication.cookies.SID}; LOGIN_INFO=${config.search.sources.youtube.authentication.cookies.LOGIN_INFO}` |
|
} : {}) |
|
}, |
|
body: { |
|
context: ytContext, |
|
videoId: decodedTrack.identifier, |
|
contentCheckOk: true, |
|
racyCheckOk: true, |
|
params: 'CgIQBg' |
|
}, |
|
method: 'POST' |
|
}) |
|
|
|
if (video.playabilityStatus.status !== 'OK') { |
|
debugLog('loadlyrics', 4, { type: 2, sourceName: _getSourceName(decodedTrack.sourceName), track: { title: decodedTrack.title, author: decodedTrack.author }, message: video.playabilityStatus.reason }) |
|
|
|
return resolve({ |
|
loadType: 'error', |
|
data: { |
|
message: video.playabilityStatus.reason, |
|
severity: 'common', |
|
cause: 'Unknown' |
|
} |
|
}) |
|
} |
|
|
|
if (!video.captions) |
|
return resolve(null) |
|
|
|
const selectedCaption = video.captions.playerCaptionsTracklistRenderer.captionTracks.find((caption) => { |
|
return caption.languageCode === language |
|
}) |
|
|
|
if (selectedCaption) { |
|
const { body: captionData } = await makeRequest(selectedCaption.baseUrl.replace('&fmt=srv3', '&fmt=json3'), { method: 'GET' }).catch((err) => { |
|
debugLog('loadlyrics', 4, { type: 2, sourceName: _getSourceName(decodedTrack.sourceName), track: { title: decodedTrack.title, author: decodedTrack.author }, message: err.message }) |
|
|
|
return resolve({ |
|
loadType: 'error', |
|
data: { |
|
message: err.message, |
|
severity: 'common', |
|
cause: 'Unknown' |
|
} |
|
}) |
|
}) |
|
|
|
const captionEvents = [] |
|
captionData.events.forEach((event) => { |
|
if (!event.segs) return null |
|
|
|
captionEvents.push({ |
|
startTime: event.tStartMs, |
|
endTime: event.tStartMs + (event.dDurationMs || 0), |
|
text: event.segs ? event.segs.map((seg) => seg.utf8).join('') : null |
|
}) |
|
}) |
|
|
|
return resolve({ |
|
loadType: 'lyricsSingle', |
|
data: { |
|
name: selectedCaption.languageCode, |
|
synced: true, |
|
data: captionEvents, |
|
rtl: !!selectedCaption.rtl |
|
} |
|
}) |
|
} else { |
|
const captions = [] |
|
let i = 0 |
|
|
|
video.captions.playerCaptionsTracklistRenderer.captionTracks.forEach(async (caption) => { |
|
const { body: captionData } = await makeRequest(caption.baseUrl.replace('&fmt=srv3', '&fmt=json3'), { method: 'GET' }).catch((err) => { |
|
debugLog('loadlyrics', 4, { type: 2, sourceName: _getSourceName(decodedTrack.sourceName), track: { title: decodedTrack.title, author: decodedTrack.author }, message: err.message }) |
|
|
|
return resolve({ |
|
loadType: 'error', |
|
data: { |
|
message: err.message, |
|
severity: 'common', |
|
cause: 'Unknown' |
|
} |
|
}) |
|
}) |
|
|
|
const captionEvents = captionData.events.map((event) => { |
|
return { |
|
startTime: event.tStartMs, |
|
endTime: event.tStartMs + event.dDurationMs, |
|
text: event.segs ? event.segs.map((seg) => seg.utf8).join('') : null |
|
} |
|
}) |
|
|
|
captions.push({ |
|
name: caption.languageCode, |
|
synced: true, |
|
data: captionEvents, |
|
rtl: !!caption.rtl |
|
}) |
|
|
|
if (++i === video.captions.playerCaptionsTracklistRenderer.captionTracks.length) { |
|
if (captions.length === 0) { |
|
debugLog('loadlyrics', 4, { type: 3, sourceName: _getSourceName(decodedTrack.sourceName), track: { title: decodedTrack.title, author: decodedTrack.author }, message: 'No captions found.' }) |
|
|
|
return resolve(null) |
|
} |
|
|
|
debugLog('loadlyrics', 4, { type: 2, sourceName: _getSourceName(decodedTrack.sourceName), track: { title: decodedTrack.title, author: decodedTrack.author } }) |
|
|
|
return resolve({ |
|
loadType: 'lyricsMultiple', |
|
data: captions |
|
}) |
|
} |
|
}) |
|
} |
|
}) |
|
} |
|
|
|
async function loadStream(url) { |
|
return new Promise(async (resolve) => { |
|
const stream = new PassThrough() |
|
await loadHLSPlaylist(url, stream) |
|
|
|
resolve(stream) |
|
}) |
|
} |
|
|
|
export default { |
|
init, |
|
free, |
|
search, |
|
loadFrom, |
|
retrieveStream, |
|
loadLyrics, |
|
loadStream |
|
} |