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