Spaces:
Runtime error
Runtime error
import fs from 'node:fs' | |
import { PassThrough } from 'node:stream' | |
import config from '../config.js' | |
import bandcamp from './sources/bandcamp.js' | |
import deezer from './sources/deezer.js' | |
import httpSource from './sources/http.js' | |
import local from './sources/local.js' | |
import pandora from './sources/pandora.js' | |
import soundcloud from './sources/soundcloud.js' | |
import spotify from './sources/spotify.js' | |
import youtube from './sources/youtube.js' | |
import genius from './sources/genius.js' | |
import musixmatch from './sources/musixmatch.js' | |
import searchWithDefault from './sources/default.js' | |
import { debugLog, http1makeRequest, makeRequest } from './utils.js' | |
async function getTrackURL(track, toDefault) { | |
switch (track.sourceName === 'pandora' || toDefault ? config.search.defaultSearchSource : track.sourceName) { | |
case 'spotify': { | |
const result = await searchWithDefault(`${track.title} - ${track.author}`, false) | |
if (result.loadType === 'error') { | |
return { | |
exception: result.data | |
} | |
} | |
if (result.loadType === 'empty') { | |
return { | |
exception: { | |
message: 'Failed to retrieve stream from source. (Spotify track not found)', | |
severity: 'common', | |
cause: 'Spotify track not found' | |
} | |
} | |
} | |
const trackInfo = result.data[0].info | |
return getTrackURL(trackInfo, true) | |
} | |
case 'ytmusic': | |
case 'youtube': { | |
return youtube.retrieveStream(track.identifier, track.sourceName, track.title) | |
} | |
case 'local': { | |
return { url: track.uri, protocol: 'file', format: 'arbitrary' } | |
} | |
case 'http': | |
case 'https': { | |
return { url: track.uri, protocol: track.sourceName, format: 'arbitrary' } | |
} | |
case 'soundcloud': { | |
return soundcloud.retrieveStream(track.identifier, track.title) | |
} | |
case 'bandcamp': { | |
return bandcamp.retrieveStream(track.uri, track.title) | |
} | |
case 'deezer': { | |
return deezer.retrieveStream(track.identifier, track.title) | |
} | |
default: { | |
return { | |
exception: { | |
message: 'Unknown source', | |
severity: 'common', | |
cause: 'Not supported source.' | |
} | |
} | |
} | |
} | |
} | |
function getTrackStream(decodedTrack, url, protocol, additionalData) { | |
return new Promise(async (resolve) => { | |
if (protocol === 'file') { | |
const file = fs.createReadStream(url) | |
file.on('error', () => { | |
debugLog('retrieveStream', 4, { type: 2, sourceName: decodedTrack.sourceName, query: decodedTrack.title, message: 'Failed to retrieve stream from source. (File not found or not accessible)' }) | |
return resolve({ | |
status: 1, | |
exception: { | |
message: 'Failed to retrieve stream from source. (File not found or not accessible)', | |
severity: 'common', | |
cause: 'No permission to access file or doesn\'t exist' | |
} | |
}) | |
}) | |
file.on('open', () => { | |
resolve({ | |
stream: file, | |
type: 'arbitrary' | |
}) | |
}) | |
} else { | |
let trueSource = [ 'pandora', 'spotify' ].includes(decodedTrack.sourceName) ? config.search.defaultSearchSource : decodedTrack.sourceName | |
if (trueSource === 'youtube' && protocol === 'hls') { | |
return resolve({ | |
stream: await youtube.loadStream(url) | |
}) | |
} | |
if (trueSource === 'deezer') { | |
return resolve({ | |
stream: await deezer.loadTrack(decodedTrack.title, url, additionalData) | |
}) | |
} | |
if (trueSource === 'soundcloud') { | |
if (additionalData === true) { | |
trueSource = config.search.fallbackSearchSource | |
} else if (protocol === 'hls') { | |
const stream = await soundcloud.loadHLSStream(url) | |
return resolve({ | |
stream | |
}) | |
} | |
} | |
const res = await ((trueSource === 'youtube' || trueSource === 'ytmusic') ? http1makeRequest : makeRequest)(url, { | |
headers: { | |
'User-Agent': 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' | |
}, | |
method: 'GET', | |
streamOnly: true | |
}) | |
if (res.statusCode !== 200) { | |
res.stream.emit('end') /* (http1)makeRequest will handle this automatically */ | |
debugLog('retrieveStream', 4, { type: 2, sourceName: decodedTrack.sourceName, query: decodedTrack.title, message: `Expected 200, received ${res.statusCode}.` }) | |
return resolve({ | |
status: 1, | |
exception: { | |
message: `Failed to retrieve stream from source. Expected 200, received ${res.statusCode}.`, | |
severity: 'suspicious', | |
cause: 'Wrong status code' | |
} | |
}) | |
} | |
const stream = new PassThrough() | |
res.stream.on('data', (chunk) => stream.write(chunk)) | |
res.stream.on('end', () => stream.end()) | |
res.stream.on('error', (error) => { | |
debugLog('retrieveStream', 4, { type: 2, sourceName: decodedTrack.sourceName, query: decodedTrack.title, message: error.message }) | |
resolve({ | |
status: 1, | |
exception: { | |
message: error.message, | |
severity: 'fault', | |
cause: 'Unknown' | |
} | |
}) | |
}) | |
resolve({ | |
stream | |
}) | |
} | |
}) | |
} | |
async function loadTracks(identifier) { | |
const ytSearch = config.search.sources.youtube ? identifier.startsWith('ytsearch:') : null | |
const ytRegex = config.search.sources.youtube && !ytSearch ? /^(?:(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:shorts\/(?:\?v=)?[a-zA-Z0-9_-]{11}|playlist\?list=[a-zA-Z0-9_-]+|watch\?(?=.*v=[a-zA-Z0-9_-]{11})[^\s]+))|(?:https?:\/\/)?(?:www\.)?youtu\.be\/[a-zA-Z0-9_-]{11})/.test(identifier) : null | |
if (config.search.sources.youtube && (ytSearch || ytRegex)) | |
return ytSearch ? youtube.search(identifier.replace('ytsearch:', ''), 'youtube', true) : youtube.loadFrom(identifier, 'youtube') | |
const ytMusicSearch = config.search.sources.youtube ? identifier.startsWith('ytmsearch:') : null | |
const ytMusicRegex = config.search.sources.youtube && !ytMusicSearch ? /^(https?:\/\/)?(music\.)?youtube\.com\/(?:shorts\/(?:\?v=)?[a-zA-Z0-9_-]{11}|playlist\?list=[a-zA-Z0-9_-]+|watch\?(?=.*v=[a-zA-Z0-9_-]{11})[^\s]+)$/.test(identifier) : null | |
if (config.search.sources.youtube && (ytMusicSearch || ytMusicRegex)) | |
return ytMusicSearch ? youtube.search(identifier.replace('ytmsearch:', ''), 'ytmusic', true) : youtube.loadFrom(identifier, 'ytmusic') | |
const spSearch = config.search.sources.spotify.enabled ? identifier.startsWith('spsearch:') : null | |
const spRegex = config.search.sources.spotify.enabled && !spSearch ? /^https?:\/\/(?:open\.spotify\.com\/|spotify:)(?:[^?]+)?(track|playlist|artist|episode|show|album)[/:]([A-Za-z0-9]+)/.exec(identifier) : null | |
if (config.search.sources[config.search.defaultSearchSource] && (spSearch || spRegex)) | |
return spSearch ? spotify.search(identifier.replace('spsearch:', '')) : spotify.loadFrom(identifier, spRegex) | |
const dzSearch = config.search.sources.deezer.enabled ? identifier.startsWith('dzsearch:') : null | |
const dzRegex = config.search.sources.deezer.enabled && !dzSearch ? /^https?:\/\/(?:www\.)?deezer\.com\/(?:[a-z]{2}\/)?(track|album|playlist)\/(\d+)/.exec(identifier) : null | |
if (config.search.sources.deezer.enabled && (dzSearch || dzRegex)) | |
return dzSearch ? deezer.search(identifier.replace('dzsearch:', ''), true) : deezer.loadFrom(identifier, dzRegex) | |
const scSearch = config.search.sources.soundcloud.enabled ? identifier.startsWith('scsearch:') : null | |
const scRegex = config.search.sources.soundcloud.enabled && !scSearch ? /^(https?:\/\/)?(www.)?(m\.)?soundcloud\.com\/[\w\-\.]+(\/)+[\w\-\.]+?$/.test(identifier) : null | |
if (config.search.sources.soundcloud.enabled && (scSearch || scRegex)) | |
return scSearch ? soundcloud.search(identifier.replace('scsearch:', ''), true) : soundcloud.loadFrom(identifier) | |
const bcSearch = config.search.sources.bandcamp ? identifier.startsWith('bcsearch:') : null | |
const bcRegex = config.search.sources.bandcamp && !bcSearch ? /^https?:\/\/[\w-]+\.bandcamp\.com(\/(track|album)\/[\w-]+)?/.test(identifier) : null | |
if (config.search.sources.bandcamp && (bcSearch || bcRegex)) | |
return bcSearch ? bandcamp.search(identifier.replace('bcsearch:', ''), true) : bandcamp.loadFrom(identifier) | |
const pdSearch = config.search.sources.pandora ? identifier.startsWith('pdsearch:') : null | |
const pdRegex = config.search.sources.pandora && !pdSearch ? /^https:\/\/www\.pandora\.com\/(?:playlist|station|podcast|artist)\/.+/.exec(identifier) : null | |
if (config.search.sources.pandora && (pdSearch || pdRegex)) | |
return pdSearch ? pandora.search(identifier.replace('pdsearch:', '')) : pandora.loadFrom(identifier) | |
if (config.search.sources.http && (identifier.startsWith('http://') || identifier.startsWith('https://'))) | |
return httpSource.loadFrom(identifier) | |
if (config.search.sources.local && identifier.startsWith('local:')) | |
return local.loadFrom(identifier.replace('local:', '')) | |
debugLog('loadTracks', 1, { params: identifier, error: 'No possible search source found.' }) | |
return { loadType: 'empty', data: {} } | |
} | |
function loadLyrics(parsedUrl, req, decodedTrack, language, fallback) { | |
return new Promise(async (resolve) => { | |
let captions = { loadType: 'empty', data: {} } | |
switch (fallback ? config.search.lyricsFallbackSource : decodedTrack.sourceName) { | |
case 'ytmusic': | |
case 'youtube': { | |
if (!config.search.sources.youtube) { | |
debugLog('loadlyrics', 1, { params: parsedUrl.pathname, headers: req.headers, error: 'No possible search source found.' }) | |
break | |
} | |
captions = await youtube.loadLyrics(decodedTrack, language) || captions | |
if (captions.loadType === 'error') | |
captions = await loadLyrics(parsedUrl, req, decodedTrack, language, true) | |
break | |
} | |
case 'spotify': { | |
if (!config.search.sources[config.search.defaultSearchSource] || !config.search.sources.spotify.enabled) { | |
debugLog('loadlyrics', 1, { params: parsedUrl.pathname, headers: req.headers, error: 'No possible search source found.' }) | |
break | |
} | |
if (config.search.sources.spotify.sp_dc === 'DISABLED') | |
return resolve(loadLyrics(parsedUrl, decodedTrack, language, true)) | |
captions = await spotify.loadLyrics(decodedTrack, language) || captions | |
if (captions.loadType === 'error') | |
captions = await loadLyrics(parsedUrl, req, decodedTrack, language, true) | |
break | |
} | |
case 'deezer': { | |
if (!config.search.sources.deezer.enabled) { | |
debugLog('loadlyrics', 1, { params: parsedUrl.pathname, headers: req.headers, error: 'No possible search source found.' }) | |
break | |
} | |
if (config.search.sources.deezer.arl === 'DISABLED') | |
return resolve(loadLyrics(parsedUrl, decodedTrack, language, true)) | |
captions = await deezer.loadLyrics(decodedTrack, language) || captions | |
if (captions.loadType === 'error') | |
captions = await loadLyrics(parsedUrl, req, decodedTrack, language, true) | |
break | |
} | |
case 'genius': { | |
if (!config.search.sources.genius.enabled) { | |
debugLog('loadlyrics', 1, { params: parsedUrl.pathname, headers: req.headers, error: 'No possible search source found.' }) | |
break | |
} | |
captions = await genius.loadLyrics(decodedTrack, language) || captions | |
break | |
} | |
case 'musixmatch': { | |
if (!config.search.sources.musixmatch.enabled) { | |
debugLog('loadlyrics', 1, { params: parsedUrl.pathname, headers: req.headers, error: 'No possible search source found.' }) | |
break | |
} | |
captions = await musixmatch.loadLyrics(decodedTrack, language) || captions | |
break | |
} | |
default: { | |
captions = await loadLyrics(parsedUrl, req, decodedTrack, language, true) | |
} | |
} | |
resolve(captions) | |
}) | |
} | |
export default { | |
getTrackURL, | |
getTrackStream, | |
loadTracks, | |
loadLyrics, | |
bandcamp, | |
deezer, | |
http: httpSource, | |
local, | |
pandora, | |
soundcloud, | |
spotify, | |
youtube, | |
genius, | |
musixmatch | |
} | |