spotify-dl/index.js
2023-04-17 16:26:49 -05:00

164 lines
3.8 KiB
JavaScript
Executable file

#!/usr/bin/env node
import * as dotenv from 'dotenv'
import { spawn } from 'child_process'
import fs from 'fs'
import Librespot from 'librespot'
import { stat as fsstat } from 'fs/promises'
import path from 'path'
const configPath = path.join(
process.env.XDG_CONFIG_HOME ?? `${process.env.HOME}/.config/`,
'spotify-dl.conf'
)
async function fileExists(path) {
try {
await fsstat(path)
} catch (error) {
return false
}
return true
}
function sanitiseFilename(filename) {
const illegalRe = /[\/\?<>\\:\*\|"]/g
const controlRe = /[\x00-\x1f\x80-\x9f]/g
const reservedRe = /^\.+$/
const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i
const windowsTrailingRe = /[\. ]+$/
const replacement = ''
return filename
.replace(illegalRe, replacement)
.replace(controlRe, replacement)
.replace(reservedRe, replacement)
.replace(windowsReservedRe, replacement)
.replace(windowsTrailingRe, replacement)
}
function formatPath(path, metadata) {
return path
.replace(
/%album_artist/gm,
sanitiseFilename(metadata.album.artists[0].name)
)
.replace(/%album_title/gm, sanitiseFilename(metadata.album.name))
.replace(/%artist/gm, sanitiseFilename(metadata.artists[0].name))
.replace(/%title/gm, sanitiseFilename(metadata.name))
.replace(/%track/gm, metadata.trackNumber)
.replace(/%disc/gm, metadata.discNumber)
.replace(/%ext/gm, 'mp3')
}
async function writeWithMetadata(track) {
let filepath
if (process.env.DL_PATH) {
filepath = formatPath(process.env.DL_PATH, track.metadata)
} else {
const newFilename = sanitiseFilename(
`${track.metadata.artists[0].name} - ${track.metadata.name}.mp3`
)
filepath = path.join(process.env.PWD, newFilename)
}
await fs.promises.mkdir(path.dirname(filepath), {
recursive: true
})
const ffmpegProc = spawn('ffmpeg', [
'-hide_banner',
'-loglevel',
'error',
'-thread_queue_size',
'1024',
'-i',
'-',
'-i',
track.metadata.album.coverArtwork[0].url,
'-map',
'0:a',
'-map',
'1:0',
'-c:1',
'copy',
// metadata
'-id3v2_version',
'3',
'-metadata:s:v',
'title=Album cover',
'-metadata:s:v',
'comment=Cover (front)',
'-metadata',
`title=${track.metadata.name}`,
'-metadata',
`artist=${track.metadata.artists[0].name}`,
'-metadata',
`album=${track.metadata.album.name}`,
'-metadata',
`album_artist=${track.metadata.album.artists[0].name}`,
'-metadata',
`track=${track.metadata.trackNumber}`,
'-metadata',
`disc=${track.metadata.discNumber}`,
// output
filepath
])
track.stream.pipe(ffmpegProc.stdin)
ffmpegProc.stdout.pipe(process.stdout)
ffmpegProc.stderr.pipe(process.stderr)
}
async function start() {
if (await fileExists(configPath)) {
dotenv.config({ path: configPath })
}
if (!process.env.SPOTIFY_USERNAME || !process.env.SPOTIFY_PASSWORD) {
throw new Error('No SPOTIFY_USERNAME or SPOTIFY_PASSWORD set.')
}
const args = process.argv.slice(2)
const spotify = new Librespot({
sessionOptions: {
handshakeOptions: {
product: 0
}
}
})
await spotify.login(
process.env.SPOTIFY_USERNAME,
process.env.SPOTIFY_PASSWORD
)
const response = await spotify.get.byUrl(args[0])
if (response.tracks) {
for (let i = 0; i < response.tracks.length; i++) {
process.stdout.clearLine()
process.stdout.cursorTo(0)
process.stdout.write(
`Downloading ${response.tracks[i].name} by ${
response.tracks[i].artists[0].name
} [${i + 1}/${response.tracks.length}]`
)
await writeWithMetadata(
await spotify.get.track(response.tracks[i].id)
)
await new Promise(r => setTimeout(r, 2000))
}
process.stdout.write('\n')
} else {
console.log(
`Downloading ${response.metadata.name} by ${response.metadata.artists[0].name}`
)
await writeWithMetadata(response)
}
console.log('Done!')
await spotify.disconnect()
}
start()