334 lines
8.3 KiB
JavaScript
334 lines
8.3 KiB
JavaScript
import fs from 'fs'
|
|
import {
|
|
ApplicationCommandTypes,
|
|
ApplicationCommandOptionTypes,
|
|
InteractionTypes,
|
|
Client
|
|
} from 'oceanic.js'
|
|
import Librespot from 'librespot'
|
|
import archiver from 'archiver'
|
|
import * as dotenv from 'dotenv'
|
|
import path from 'path'
|
|
import { spawn } from 'child_process'
|
|
import { formatPath, sanitiseFilename } from './utils.js'
|
|
import Filesdotgay from './filesdotgay.js'
|
|
dotenv.config()
|
|
|
|
const LOADING_EMOJI = process.env.LOADING_EMOJI ?? ''
|
|
|
|
function generateRandomId(length = 50) {
|
|
let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
|
let str = ''
|
|
for (let i = 0; i < length; i++) {
|
|
str += chars.charAt(Math.floor(Math.random() * chars.length))
|
|
}
|
|
return str
|
|
}
|
|
|
|
if (!fs.existsSync('rips')) fs.mkdirSync('rips')
|
|
const ripsPath = fs.realpathSync('rips')
|
|
|
|
const bot = new Client({
|
|
auth: 'Bot ' + process.env.DISCORD_TOKEN,
|
|
gateway: {
|
|
intents: ['GUILD_MESSAGES', 'DIRECT_MESSAGES', 'MESSAGE_CONTENT']
|
|
}
|
|
})
|
|
const filesdotgay = new Filesdotgay()
|
|
|
|
const spotify = new Librespot({
|
|
sessionOptions: {
|
|
handshakeOptions: {
|
|
product: 0
|
|
}
|
|
}
|
|
})
|
|
|
|
async function writeWithMetadata(track, downloadPathFormat, extension = 'mp3') {
|
|
let filepath = formatPath(downloadPathFormat, track.metadata, extension)
|
|
fs.mkdirSync(path.dirname(filepath), {
|
|
recursive: true
|
|
})
|
|
if (extension != 'mp3') {
|
|
return new Promise(resolve => {
|
|
let writeStream = fs.createWriteStream(filepath)
|
|
track.stream.pipe(writeStream)
|
|
track.stream.once('end', resolve)
|
|
})
|
|
}
|
|
|
|
let args = [
|
|
'-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
|
|
]
|
|
|
|
return new Promise((resolve, reject) => {
|
|
console.log('ffmpeg', ...args.map(e => `'${e}'`))
|
|
const ffmpegProc = spawn('ffmpeg', args)
|
|
|
|
track.stream.pipe(ffmpegProc.stdin)
|
|
|
|
ffmpegProc.stdout.pipe(process.stdout)
|
|
ffmpegProc.stderr.pipe(process.stderr)
|
|
ffmpegProc.once('exit', code => {
|
|
if (code == 1) reject()
|
|
resolve()
|
|
})
|
|
})
|
|
}
|
|
|
|
async function downloadAndUpdateMessage(
|
|
url,
|
|
interaction,
|
|
originalMessage,
|
|
ogg
|
|
) {
|
|
const item = await spotify.get.byUrl(url)
|
|
let metadata = {
|
|
title: item.name ?? item.title ?? item.metadata.name,
|
|
author:
|
|
item.owner?.name ??
|
|
item.publisher ??
|
|
(item.artists ?? item.metadata.artists).map(e => e.name).join(', ')
|
|
}
|
|
let itemName = `*${metadata.title}* by *${metadata.author}*`
|
|
let messageStartText = `Downloading ${itemName} ${LOADING_EMOJI}`
|
|
|
|
let randomId = generateRandomId()
|
|
const itemFolderPath = `${ripsPath}/${randomId}`
|
|
await fs.promises.mkdir(itemFolderPath)
|
|
|
|
await interaction.editFollowup(originalMessage.id, {
|
|
content: messageStartText
|
|
})
|
|
|
|
let outputPath
|
|
let friendlyFilename
|
|
|
|
const extension = ogg ? 'ogg' : 'mp3'
|
|
|
|
if (item.tracks) {
|
|
const hasMultipleDiscs =
|
|
(item.tracks[item.tracks.length - 1].discNumber ?? 1) != 1
|
|
const downloadFormat =
|
|
itemFolderPath +
|
|
(hasMultipleDiscs
|
|
? '/%disc %track %title - %artist.%ext'
|
|
: '/%track %title - %artist.%ext')
|
|
for (let i = 0; i < item.tracks.length; i++) {
|
|
await interaction.editFollowup(originalMessage.id, {
|
|
content: `${messageStartText}\n${item.tracks[i].name} by ${
|
|
item.tracks[i].artists[0].name
|
|
} [${i + 1}/${item.tracks.length}]`
|
|
})
|
|
await writeWithMetadata(
|
|
await spotify.get.track(item.tracks[i].id),
|
|
downloadFormat,
|
|
extension
|
|
)
|
|
await new Promise(r => setTimeout(r, 500))
|
|
}
|
|
await interaction.editFollowup(originalMessage.id, {
|
|
content: `Downloaded ${itemName}.\nCompressing to .zip file ${LOADING_EMOJI}`
|
|
})
|
|
outputPath = `${ripsPath}/${randomId}.zip`
|
|
friendlyFilename =
|
|
metadata.title.replace(/[^a-zA-z0-9 \-_]/gm, '-') + '.zip'
|
|
const output = fs.createWriteStream(outputPath)
|
|
const archive = archiver('zip', {
|
|
zlib: { level: 9 } // Sets the compression level.
|
|
})
|
|
archive.directory(itemFolderPath, false)
|
|
archive.pipe(output)
|
|
archive.finalize()
|
|
await new Promise((resolve, reject) => {
|
|
output.on('close', resolve)
|
|
archive.on('error', reject)
|
|
})
|
|
fs.promises.rm(itemFolderPath, { recursive: true })
|
|
} else {
|
|
const downloadFormat = itemFolderPath + '/%title - %artist.%ext'
|
|
await writeWithMetadata(item, downloadFormat, extension)
|
|
friendlyFilename = (await fs.promises.readdir(itemFolderPath))[0]
|
|
outputPath = itemFolderPath + '/' + friendlyFilename
|
|
}
|
|
|
|
const fileSizeMib = (await fs.promises.stat(outputPath)).size / 1048576
|
|
const humanFileSize = `${new Intl.NumberFormat().format(
|
|
Math.round(fileSizeMib * 10) / 10
|
|
)} MiB`
|
|
|
|
const useDiscordUpload = fileSizeMib <= 25
|
|
|
|
if (useDiscordUpload) {
|
|
await interaction.editFollowup(originalMessage.id, {
|
|
content: `Uploading ${itemName} (${humanFileSize}) ${LOADING_EMOJI}`
|
|
})
|
|
await interaction.editFollowup(originalMessage.id, {
|
|
content: itemName,
|
|
attachments: [
|
|
{
|
|
id: '0',
|
|
filename: friendlyFilename,
|
|
description: `${metadata.title} by ${metadata.author}`
|
|
}
|
|
],
|
|
files: [
|
|
{
|
|
name: 'file[0]',
|
|
filename: friendlyFilename,
|
|
contents: await fs.promises.readFile(outputPath)
|
|
}
|
|
]
|
|
})
|
|
} else {
|
|
interaction.editFollowup(originalMessage.id, {
|
|
content: `Uploading ${itemName} to files.gay (${humanFileSize}) ${LOADING_EMOJI}`
|
|
})
|
|
let uploadUrl
|
|
try {
|
|
uploadUrl = await filesdotgay.upload(
|
|
await fs.promises.readFile(outputPath),
|
|
sanitiseFilename(`${metadata.title} - ${metadata.author}.zip`)
|
|
)
|
|
} catch (err) {
|
|
interaction.editFollowup(originalMessage.id, {
|
|
content: `An error occurred while uploading: \`\`\`${err.message.replaceAll(
|
|
'`',
|
|
'\\`'
|
|
)}\`\`\``
|
|
})
|
|
}
|
|
interaction.editFollowup(originalMessage.id, {
|
|
content: `${itemName}\n${uploadUrl}`
|
|
})
|
|
}
|
|
await fs.promises.rm(outputPath)
|
|
}
|
|
|
|
bot.on('interactionCreate', async interaction => {
|
|
if (interaction.type !== InteractionTypes.APPLICATION_COMMAND) return
|
|
if (interaction.data.type !== ApplicationCommandTypes.CHAT_INPUT) return
|
|
await interaction.defer()
|
|
|
|
if (interaction.data.name === 'dl') {
|
|
try {
|
|
let url = interaction.data.options.getString('url', true)
|
|
let ogg = interaction.data.options.getBoolean('ogg') ?? false
|
|
|
|
let urlObj
|
|
try {
|
|
urlObj = new URL(url)
|
|
} catch (error) {
|
|
await interaction.createFollowup({
|
|
content: 'Your second argument is not a valid URL!'
|
|
})
|
|
return
|
|
}
|
|
if (!url.host === 'open.spotify.com') {
|
|
await interaction.createFollowup({
|
|
content: 'Your second argument is not a Spotify URL!'
|
|
})
|
|
return
|
|
}
|
|
urlObj.search = ''
|
|
url = urlObj.href
|
|
let originalMessage = await interaction.createFollowup({
|
|
content: 'Loading...'
|
|
})
|
|
try {
|
|
await downloadAndUpdateMessage(
|
|
urlObj.href,
|
|
interaction,
|
|
originalMessage,
|
|
ogg
|
|
)
|
|
} catch (err) {
|
|
console.log(err)
|
|
await interaction.editFollowup(originalMessage.id, {
|
|
content: `Error occured: ${err.message}`
|
|
})
|
|
}
|
|
} catch (error) {
|
|
console.log(error)
|
|
}
|
|
}
|
|
})
|
|
|
|
bot.connect()
|
|
|
|
bot.on('ready', async () => {
|
|
console.log('Logged into Discord')
|
|
await spotify.login(
|
|
process.env.SPOTIFY_USERNAME,
|
|
process.env.SPOTIFY_PASSWORD
|
|
)
|
|
console.log('Logged into Spotify')
|
|
await filesdotgay.login(
|
|
process.env.FILES_GAY_USER,
|
|
process.env.FILES_GAY_PASS
|
|
)
|
|
console.log('Logged into files.gay')
|
|
await bot.application.bulkEditGlobalCommands([
|
|
{
|
|
type: ApplicationCommandTypes.CHAT_INPUT,
|
|
name: 'dl',
|
|
dmPermission: true,
|
|
description: 'Download',
|
|
defaultMemberPermissions: '2048',
|
|
options: [
|
|
{
|
|
type: ApplicationCommandOptionTypes.STRING,
|
|
name: 'url',
|
|
description: 'The URL of the Spotify resource',
|
|
required: true
|
|
},
|
|
{
|
|
type: ApplicationCommandOptionTypes.BOOLEAN,
|
|
name: 'ogg',
|
|
description: 'Use OGG format'
|
|
}
|
|
]
|
|
}
|
|
])
|
|
console.log('Ready as', bot.user.tag)
|
|
})
|
|
|
|
bot.on('error', err => {
|
|
console.error(err)
|
|
})
|