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 | |
| } | |