Last active
July 30, 2019 11:09
-
-
Save szkrd/e8b3f5307e02f0ac5b928d35b7dd3a5a to your computer and use it in GitHub Desktop.
save bandcamp album tracks to a pls playlist
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// save bandcamp album tracks to a pls playlist; | |
// mp3 file urls are protected by a token, so these playlist | |
// will "expire", but at least one can use a native player; | |
// still, please do read http://bandcamp.com/help/audio_basics#steal | |
// and http://bandcamp.com/terms_of_use | |
// | |
// ``` | |
// mkdir bandcamp-pls && \ | |
// cd bandcamp-pls && \ | |
// npm init --yes && \ | |
// npm i -SE node-fetch safe-eval filenamify object-get open && \ | |
// mkdir playlists | |
// ``` | |
const fetch = require('node-fetch') | |
const safeEval = require('safe-eval') | |
const filenamify = require('filenamify') | |
const objectGet = require('object-get') | |
const open = require('open') | |
const fs = require('fs') | |
const path = require('path') | |
const { promisify } = require('util') | |
const writeFileAsync = promisify(fs.writeFile) | |
const targetUrl = process.argv.slice(2).filter(param => param.startsWith('http'))[0] || '' | |
const openPlaylist = process.argv.includes('--open') | |
async function main() { | |
if (!targetUrl || !targetUrl.includes('bandcamp.com')) { | |
throw new Error('usage: node bandcamp-pls (--open) [url]') | |
} | |
// fetch will use a custom user-agent, which I'm not going to override | |
const page = await fetch(targetUrl) | |
const text = await page.text() | |
const lines = text.split(/\n/) | |
let jsonString = [] | |
let inRange = false | |
let successful = false | |
for (let i = 0; i < lines.length; i++) { | |
const line = lines[i] | |
const trimmed = lines[i].trim() | |
// search for track and album data global variable | |
if (trimmed.startsWith('var TralbumData = {')) { | |
jsonString.push('{') | |
inRange = true | |
} | |
if (inRange && /^ {4}[a-z_]*?:/.test(line) && !trimmed.startsWith('//')) { | |
jsonString.push(trimmed.replace(/\s+\/\/\s+.*$/gi, '')) | |
} | |
if (inRange && trimmed === '};') { | |
jsonString.push('}') | |
jsonString = jsonString.join('') | |
successful = true | |
break | |
} | |
} | |
if (!successful || !jsonString) { | |
throw new Error('!album data not found') | |
} | |
const albumData = safeEval(jsonString) || {} | |
const title = (objectGet(albumData, 'current.title') || 'unknown').trim() | |
const description = objectGet(albumData, 'current.credits') || '' | |
const defaultPrice = objectGet(albumData, 'current.defaultPrice') | |
const setPrice = objectGet(albumData, 'current.set_price') | |
const price = defaultPrice || setPrice || 0 | |
console.info('\n# ' + title) | |
if (description) { | |
console.info('\n' + description) | |
} | |
if (price) { | |
console.info(`\n_If you like this album, you can have it for $${price}_`) | |
} | |
const trackInfo = objectGet(albumData, 'trackinfo') || [] | |
if (!trackInfo.length) { | |
throw new Error('!no trackinfo found') | |
} | |
const output = trackInfo.reduce( | |
(playlist, track, i) => { | |
const n = i + 1 | |
const len = Math.round(track.duration * 60) | |
const file = track.file['mp3-128'] | |
const title = track.title | |
playlist.push(`File${n}=${file}`) | |
playlist.push(`Title${n}=${title}`) | |
playlist.push(`Length${n}=${len}`) | |
return playlist | |
}, | |
['[playlist]'] | |
) | |
output.push(`NumberOfEntries=${trackInfo.length}`) | |
output.push('Version=2') | |
const fileName = filenamify(title.toLocaleLowerCase()) + '.pls' | |
const fileNameWithPath = path.resolve(__dirname, './playlists/' + fileName) | |
await writeFileAsync(fileNameWithPath, output.join('\n'), 'utf-8') | |
console.info(`\n"${fileName}" saved`) | |
if (openPlaylist) { | |
await open(fileNameWithPath) | |
} | |
} | |
main().catch((error) => { | |
if (error.message.startsWith('!')) { | |
console.error(error.message.replace(/^!/, '')) | |
} else { | |
console.error(error) | |
} | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment