164 lines
3.8 KiB
JavaScript
Executable file
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()
|