File size: 12,289 Bytes
b58c6cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
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
}