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