nodelink / src /sources.js
flameface's picture
Upload 25 files
b58c6cb verified
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
}