Spaces:
Runtime error
Runtime error
| import config from '../../config.js' | |
| import { debugLog, makeRequest, encodeTrack } from '../utils.js' | |
| async function loadFrom(url) { | |
| const { body: data } = await makeRequest(url, { method: 'GET' }) | |
| const matches = /<script type="application\/ld\+json">([\s\S]*?)<\/script>/.exec(data) | |
| if (!matches.length) { | |
| debugLog('loadtracks', 4, { type: 2, loadType: 'empty', sourceName: 'BandCamp', query: url, message: 'No matches found.' }) | |
| return { | |
| loadType: 'empty', | |
| data: {} | |
| } | |
| } | |
| const trackInfo = JSON.parse(matches[1]) | |
| debugLog('loadtracks', 4, { type: 1, loadType: trackInfo['@type'] === 'MusicRecording' ? 'track' : 'album', sourceName: 'BandCamp', query: url }) | |
| switch (trackInfo['@type']) { | |
| case 'MusicRecording': { | |
| const identifier = trackInfo['@id'].match(/^https?:\/\/([^/]+)\/track\/([^/?]+)/) | |
| const track = { | |
| identifier: `${identifier[1]}:${identifier[2]}`, | |
| isSeekable: true, | |
| author: trackInfo.byArtist.name, | |
| length: (trackInfo.duration.split('P')[1].split('H')[0] * 3600000) + (trackInfo.duration.split('H')[1].split('M')[0] * 60000) + (trackInfo.duration.split('M')[1].split('S')[0] * 1000), | |
| isStream: false, | |
| position: 0, | |
| title: trackInfo.name, | |
| uri: trackInfo['@id'], | |
| artworkUrl: trackInfo.image, | |
| isrc: null, | |
| sourceName: 'bandcamp' | |
| } | |
| debugLog('loadtracks', 4, { type: 2, loadType: 'track', sourceName: 'BandCamp', track, query: url }) | |
| return { | |
| loadType: 'track', | |
| data: { | |
| encoded: encodeTrack(track), | |
| info: track, | |
| pluginInfo: {} | |
| } | |
| } | |
| } | |
| case 'MusicAlbum': { | |
| const tracks = [] | |
| trackInfo.track.itemListElement.forEach((item) => { | |
| const identifier = item.item['@id'].match(/^https?:\/\/([^/]+)\/track\/([^/?]+)/) | |
| const track = { | |
| identifier: `${identifier[1]}:${identifier[2]}`, | |
| isSeekable: true, | |
| author: trackInfo.byArtist.name, | |
| length: (item.item.duration.split('P')[1].split('H')[0] * 3600000) + (item.item.duration.split('H')[1].split('M')[0] * 60000) + (item.item.duration.split('M')[1].split('S')[0] * 1000), | |
| isStream: false, | |
| position: 0, | |
| title: item.item.name, | |
| uri: item.item['@id'], | |
| artworkUrl: trackInfo.image, | |
| isrc: null, | |
| sourceName: 'bandcamp' | |
| } | |
| tracks.push({ | |
| encoded: encodeTrack(track), | |
| info: track, | |
| pluginInfo: {} | |
| }) | |
| }) | |
| debugLog('loadtracks', 4, { type: 2, loadType: 'album', sourceName: 'BandCamp', playlistName: trackInfo.name }) | |
| return { | |
| loadType: 'album', | |
| data: { | |
| info: { | |
| name: trackInfo.name, | |
| selectedTrack: 0 | |
| }, | |
| tracks | |
| } | |
| } | |
| } | |
| } | |
| } | |
| async function search(query, shouldLog) { | |
| if (shouldLog) debugLog('search', 4, { type: 1, sourceName: 'BandCamp', query }) | |
| const { body: data } = await makeRequest(`https://bandcamp.com/search?q=${encodeURI(query)}&item_type=t&from=results`, { method: 'GET' }) | |
| const names = data.match(/<div class="heading">\s+<a.*?>(.*?)<\/a>/gs) | |
| if (!names) { | |
| if (shouldLog) debugLog('search', 4, { type: 2, sourceName: 'BandCamp', query, message: 'No matches found.' }) | |
| return { | |
| loadType: 'empty', | |
| data: {} | |
| } | |
| } | |
| const tracks = [] | |
| if (names.length > config.options.maxResultsLength) | |
| names = names.slice(0, config.options.maxResultsLength) | |
| names.forEach((name) => { | |
| tracks.push({ | |
| encoded: null, | |
| info: { | |
| identifier: null, | |
| isSeekable: true, | |
| author: null, | |
| length: -1, | |
| isStream: false, | |
| position: 0, | |
| title: name[1].trim(), | |
| uri: null, | |
| artworkUrl: null, | |
| isrc: null, | |
| sourceName: 'bandcamp' | |
| }, | |
| pluginInfo: {} | |
| }) | |
| }) | |
| if (!tracks.length) { | |
| if (shouldLog) debugLog('search', 4, { type: 2, sourceName: 'BandCamp', query, message: 'No matches found.' }) | |
| return { | |
| loadType: 'empty', | |
| data: {} | |
| } | |
| } | |
| const authors = data.match(/<div class="subhead">\s+(?:from\s+)?[\s\S]*?by (.*?)\s+<\/div>/gs) | |
| authors.forEach((author, i) => { | |
| tracks[i].info.author = author.split('by')[1].split('</div>')[0].trim() | |
| }) | |
| const artworkUrls = data.match(/<div class="art">\s*<img src="(.+?)"/gs) | |
| artworkUrls.forEach((artworkUrl, i) => { | |
| tracks[i].info.artworkUrl = artworkUrl.split('"')[3].split('"')[0] | |
| }) | |
| const urls = data.match(/<div class="itemurl">\s+<a.*?>(.*?)<\/a>/gs) | |
| urls.forEach((url, i) => { | |
| tracks[i].info.uri = url.split('">')[2].split('</a>')[0] | |
| const identifier = tracks[i].info.uri.match(/^https?:\/\/([^/]+)\/track\/([^/?]+)/) | |
| tracks[i].info.identifier = `${identifier[1]}:${identifier[2]}` | |
| tracks[i].encoded = encodeTrack(tracks[i].info) | |
| tracks[i].pluginInfo = {} | |
| }) | |
| if (shouldLog) debugLog('search', 4, { type: 2, sourceName: 'BandCamp', tracksLen: tracks.length, query }) | |
| return { | |
| loadType: 'search', | |
| data: tracks | |
| } | |
| } | |
| async function retrieveStream(uri, title) { | |
| const { body: data } = await makeRequest(uri, { method: 'GET' }) | |
| const streamURL = data.match(/https?:\/\/t4\.bcbits\.com\/stream\/[a-zA-Z0-9]+\/mp3-128\/\d+\?p=\d+&ts=\d+&t=[a-zA-Z0-9]+&token=\d+_[a-zA-Z0-9]+/) | |
| if (!streamURL) { | |
| debugLog('retrieveStream', 4, { type: 2, sourceName: 'BandCamp', query: title, message: 'No stream URL was found.' }) | |
| return { | |
| exception: { | |
| message: 'Failed to get the stream from source.', | |
| severity: 'fault', | |
| cause: 'Unknown' | |
| } | |
| } | |
| } | |
| return { | |
| url: streamURL[0], | |
| protocol: 'https', | |
| format: 'arbitrary' | |
| } | |
| } | |
| export default { | |
| loadFrom, | |
| search, | |
| retrieveStream | |
| } |