Spaces:
Runtime error
Runtime error
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 | |
} |