CoolFace
Apppublic

Shadowfilebot/nodelink

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
soundcloud.js367 linesDownload Raw Back to sources
1import { PassThrough } from 'node:stream'2 3import config from '../../config.js'4import { debugLog, encodeTrack, http1makeRequest, loadHLS } from '../utils.js'5import searchWithDefault from './default.js'6import sources from '../sources.js'7 8const sourceInfo = {9  clientId: null10}11 12async function init() {13  if (config.search.sources.soundcloud.clientId !== 'AUTOMATIC') {14    sourceInfo.clientId = config.search.sources.soundcloud.clientId15 16    return;17  }18 19  debugLog('soundcloud', 5, { type: 1, message: 'clientId not provided. Fetching clientId...' })20 21  const { body: mainpage } = await http1makeRequest('https://soundcloud.com', {22    method: 'GET'23  }).catch(() => {24    debugLog('soundcloud', 5, { type: 2, message: 'Failed to fetch clientId.' })25  })26 27  const assetId = mainpage.match(/https:\/\/a-v2.sndcdn.com\/assets\/([a-zA-Z0-9-]+).js/gs)[5]28 29  const { body: data } = await http1makeRequest(assetId, {30    method: 'GET'31  }).catch(() => {32    debugLog('soundcloud', 5, { type: 2, message: 'Failed to fetch clientId.' })33  })34 35  const clientId = data.match(/client_id=([a-zA-Z0-9]{32})/)[1]36 37  if (!clientId) {38    debugLog('soundcloud', 5, { type: 2, message: 'Failed to fetch clientId.' })39 40    return;41  }42 43  sourceInfo.clientId = clientId44 45  debugLog('soundcloud', 5, { type: 1, message: 'Successfully fetched clientId.' })46}47 48async function loadFrom(url) {49  let req = await http1makeRequest(`https://api-v2.soundcloud.com/resolve?url=${encodeURI(url)}&client_id=${sourceInfo.clientId}`, { method: 'GET' })50 51  if (req.error || req.statusCode !== 200) {52    const errorMessage = req.error ? req.error.message : `SoundCloud returned invalid status code: ${req.statusCode}`53 54    debugLog('loadtracks', 4, { type: 2, loadType: 'unknown', sourceName: 'Soundcloud', query: url, message: errorMessage })55 56    return {57      loadType: 'error',58      data: {59        message: errorMessage,60        severity: 'fault',61        cause: 'Unknown'62      }63    }64  }65 66  const body = req.body67 68  if (typeof body !== 'object') {69    debugLog('loadtracks', 4, { type: 3, loadType: 'unknown', sourceName: 'Soundcloud', query: url, message: 'Invalid response from SoundCloud.' })70 71    return {72      loadType: 'error',73      data: {74        message: 'Invalid response from SoundCloud.',75        severity: 'common',76        cause: 'Unknown'77      }78    }79  }80 81  debugLog('loadtracks', 4, { type: 1, loadType: body.kind || 'unknown', sourceName: 'SoundCloud', query: url })82 83  if (Object.keys(body).length === 0) {84    debugLog('loadtracks', 4, { type: 3, loadType: body.kind || 'unknown', sourceName: 'Soundcloud', query: url, message: 'No matches found.' })85 86    return {87      loadType: 'empty',88      data: {}89    }90  }91 92  switch (body.kind) {93    case 'track': {94      const track = {95        identifier: body.id.toString(),96        isSeekable: true,97        author: body.user.username,98        length: body.duration,99        isStream: false,100        position: 0,101        title: body.title,102        uri: body.permalink_url,103        artworkUrl: body.artwork_url,104        isrc: body.publisher_metadata ? body.publisher_metadata.isrc : null,105        sourceName: 'soundcloud'106      }107 108      debugLog('loadtracks', 4, { type: 2, loadType: 'track', sourceName: 'SoundCloud', track, query: url })109 110      return {111        loadType: 'track',112        data: {113          encoded: encodeTrack(track),114          info: track,115          playlistInfo: {}116        }117      }118    }119    case 'playlist': {120      const tracks = []121      const notLoaded = []122 123      if (body.tracks.length > config.options.maxAlbumPlaylistLength)124        data.tracks = body.tracks.slice(0, config.options.maxAlbumPlaylistLength)125 126      body.tracks.forEach((item) => {127        if (!item.title) {128          notLoaded.push(item.id.toString())129 130          return;131        }132 133        const track = {134          identifier: item.id.toString(),135          isSeekable: true,136          author: item.user.username,137          length: item.duration,138          isStream: false,139          position: 0,140          title: item.title,141          uri: item.permalink_url,142          artworkUrl: item.artwork_url,143          isrc: item.publisher_metadata?.isrc,144          sourceName: 'soundcloud'145        }146 147        tracks.push({148          encoded: encodeTrack(track),149          info: track,150          playlistInfo: {}151        })152      })153 154      if (notLoaded.length) {155        let stop = false156 157        while ((notLoaded.length && !stop) && (tracks.length > config.options.maxAlbumPlaylistLength)) {158          const notLoadedLimited = notLoaded.slice(0, 50)159          data = await http1makeRequest(`https://api-v2.soundcloud.com/tracks?ids=${notLoadedLimited.join('%2C')}&client_id=${sourceInfo.clientId}`, { method: 'GET' })160          data = data.body161 162          data.forEach((item) => {163            const track = {164              identifier: item.id.toString(),165              isSeekable: true,166              author: item.user.username,167              length: item.duration,168              isStream: false,169              position: 0,170              title: item.title,171              uri: item.permalink_url,172              artworkUrl: item.artwork_url,173              isrc: item.publisher_metadata ? item.publisher_metadata.isrc : null,174              sourceName: 'soundcloud'175            }176 177            tracks.push({178              encoded: encodeTrack(track),179              info: track,180              playlistInfo: {}181            })182          })183 184          notLoaded.splice(0, 50)185 186          if (notLoaded.length === 0)187            stop = true188        }189      }190 191      debugLog('loadtracks', 4, { type: 2, loadType: 'playlist', sourceName: 'SoundCloud', playlistName: data.title })192 193      return {194        loadType: 'playlist',195        data: {196          info: {197            name: data.title,198            selectedTrack: 0,199          },200          pluginInfo: {},201          tracks,202        }203      }204    }205    case 'user': {206      debugLog('loadtracks', 4, { type: 2, loadType: 'artist', sourceName: 'SoundCloud', playlistName: data.full_name })207 208      return {209        loadType: 'empty',210        data: {}211      }212    }213  }214}215 216async function search(query, shouldLog) {217  if (shouldLog) debugLog('search', 4, { type: 1, sourceName: 'SoundCloud', query })218 219  const req = await http1makeRequest(`https://api-v2.soundcloud.com/search?q=${encodeURI(query)}&variant_ids=&facet=model&user_id=992000-167630-994991-450103&client_id=${sourceInfo.clientId}&limit=${config.options.maxResultsLength}&offset=0&linked_partitioning=1&app_version=1679652891&app_locale=en`, { method: 'GET' })220  const body = req.body221 222  if (req.error || req.statusCode !== 200) {223    const errorMessage = req.error ? req.error.message : `SoundCloud returned invalid status code: ${req.statusCode}`224 225    debugLog('search', 4, { type: 2, sourceName: 'SoundCloud', query, message: errorMessage })226 227    return {228      exception: {229        message: errorMessage,230        severity: 'fault',231        cause: 'Unknown'232      }233    }234  }235 236  if (body.total_results === 0) {237    debugLog('search', 4, { type: 2, sourceName: 'SoundCloud', query, message: 'No matches found.' })238 239    return {240      loadType: 'empty',241      data: {}242    }243  }244 245  const tracks = []246 247  if (body.collection.length > config.options.maxSearchResults)248    body.collection = body.collection.filter((item, i) => i < config.options.maxSearchResults || item.kind === 'track')249 250  body.collection.forEach((item) => {251    if (item.kind !== 'track') return;252    253    const track = {254      identifier: item.id.toString(),255      isSeekable: true,256      author: item.user.username,257      length: item.duration,258      isStream: false,259      position: 0,260      title: item.title,261      uri: item.uri,262      artworkUrl: item.artwork_url,263      isrc: null,264      sourceName: 'soundcloud'265    }266 267    tracks.push({268      encoded: encodeTrack(track),269      info: track,270      pluginInfo: {}271    })272  })273 274  if (shouldLog)275    debugLog('search', 4, { type: 2, sourceName: 'SoundCloud', tracksLen: tracks.length, query })276 277  return {278    loadType: 'search',279    data: tracks280  }281}282 283async function retrieveStream(identifier, title) {284  const req = await http1makeRequest(`https://api-v2.soundcloud.com/resolve?url=https://api.soundcloud.com/tracks/${identifier}&client_id=${sourceInfo.clientId}`, { method: 'GET' })285  const body = req.body286 287  if (req.error || req.statusCode !== 200) {288    const errorMessage = req.error ? req.error.message : `SoundCloud returned invalid status code: ${req.statusCode}`289 290    debugLog('retrieveStream', 4, { type: 2, sourceName: 'SoundCloud', query: title, message: errorMessage })291 292    return {293      exception: {294        message: errorMessage,295        severity: 'fault',296        cause: 'Unknown'297      }298    }299  }300 301  if (body.errors) {302    debugLog('retrieveStream', 4, { type: 2, sourceName: 'SoundCloud', query: title, message: body.errors[0].error_message })303 304    return {305      exception: {306        message: body.errors[0].error_message,307        severity: 'fault',308        cause: 'Unknown'309      }310    }311  }312 313  const oggOpus = body.media.transcodings.find((transcoding) => transcoding.format.mime_type === 'audio/ogg; codecs="opus"')314  const transcoding = oggOpus || body.media.transcodings[0]315 316  if (transcoding.snipped && config.search.sources.soundcloud.fallbackIfSnipped) {317    debugLog('retrieveStream', 4, { type: 2, sourceName: 'SoundCloud', query: title, message: `Track is snipped, falling back to: ${config.search.fallbackSearchSource}.` })318 319    const search = await searchWithDefault(title, true)320 321    if (search.loadType === 'search') {322      const urlInfo = await sources.getTrackURL(search.data[0].info)323 324      return {325        url: urlInfo.url,326        protocol: urlInfo.protocol,327        format: urlInfo.format,328        additionalData: true329      }330    }331  }332 333  return {334    url: `${transcoding.url}?client_id=${sourceInfo.clientId}`,335    protocol: transcoding.format.protocol,336    format: oggOpus ? 'ogg/opus' : 'arbitrary'337  }338}339 340async function loadHLSStream(url) {341  const streamHlsRedirect = await http1makeRequest(url, { method: 'GET' })342 343  const stream = new PassThrough()344  await loadHLS(streamHlsRedirect.body.url, stream)345 346  return stream347}348 349async function loadFilters(url, protocol) {350  if (protocol === 'hls') {351    const streamHlsRedirect = await http1makeRequest(url, { method: 'GET' })352 353    return streamHlsRedirect.body.url354  } else {355    return url356  }357}358 359export default {360  init,361  loadFrom,362  search,363  retrieveStream,364  loadHLSStream,365  loadFilters366}367