├── LICENSE ├── README.md ├── index.js ├── install_and_run.sh ├── package-lock.json ├── package.json ├── src ├── bot.js ├── commands │ └── dl.js ├── config │ ├── bot-config.json │ ├── commands-config.json │ └── logging-config.json ├── core │ ├── command-handler │ │ ├── Ctx.js │ │ └── command-handler.js │ ├── discord-utils │ │ └── permissions-handler.js │ ├── event-handler │ │ ├── event-handler.js │ │ └── events │ │ │ └── message.js │ ├── graphics │ │ ├── colors.js │ │ └── embeds.js │ └── utils │ │ ├── README.md │ │ ├── artisthelper.js │ │ ├── logger.js │ │ ├── qobuzhelper.js │ │ ├── serviceAccountHelper.js │ │ └── zspotify │ │ └── zspotify_source_in_here.txt ├── downloads │ └── toUpload │ │ └── example.zip └── sharding.js └── zspotify-reqs.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 crackhub213 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This program is Licensed under the [MIT License](https://tldrlegal.com/license/mit-license) Meaning you can modify, distribute and sell this as much as you want. 2 | 3 | > The work is provided "as is". You may not hold the author liable. 4 | 5 | 6 | ![Figure it out fool](https://i.imgur.com/HlTilNF.png) 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Starts the sharding process 2 | require('./src/sharding'); 3 | -------------------------------------------------------------------------------- /install_and_run.sh: -------------------------------------------------------------------------------- 1 | echo "[WARNING] This will overwrite your current nodeJS version." 2 | echo "Beginning installation..." 3 | sudo apt install -y curl 4 | echo "Installed: cURL" 5 | sudo apt install -y python3-pip 6 | echo "Installed: pip" 7 | sudo apt install -y ffmpeg 8 | echo "Installed: ffmpeg" 9 | sudo apt install p7zip-full 10 | echo "Installed: 7-Zip" 11 | curl https://rclone.org/install.sh | sudo bash 12 | echo "Installed: rclone" 13 | wget https://github.com/yt-dlp/yt-dlp/releases/download/2022.02.04/yt-dlp && chmod +x yt-dlp && sudo mv yt-dlp /usr/local/bin/yt-dlp 14 | echo "Installed: yt-dlp" 15 | curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash 16 | source ~/.profile 17 | nvm install 16.13.1 18 | echo "Installed: nodeJS v16.13.1" 19 | pip3 install streamrip 20 | echo "Installed: streamrip" 21 | echo "Opening streamrip config in default editor, add your accounts and edit your settings!" 22 | rip config --open 23 | echo "Running npm install" 24 | npm install 25 | echo "Running pip3 install" 26 | pip3 install -r zspotify-reqs.txt 27 | echo "Running in production mode" 28 | npm run start 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slav-music-bot", 3 | "version": "1.0.0", 4 | "description": "supplying all slavs with HQ music.", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon index.js", 8 | "start": "node index.js" 9 | }, 10 | "author": "c213", 11 | "license": "MIT", 12 | "dependencies": { 13 | "axios": "^0.25.0", 14 | "beautify.log": "^2.1.2", 15 | "bufferutil": "^4.0.3", 16 | "cheerio": "^1.0.0-rc.10", 17 | "child_process": "^1.0.2", 18 | "discord.js-light": "^3.5.4", 19 | "erlpack": "github:discord/erlpack", 20 | "get-folder-size": "^3.1.0", 21 | "js-base64": "^3.7.2", 22 | "require-all": "^3.0.0", 23 | "short-uuid": "^4.2.0", 24 | "toml": "^3.0.0", 25 | "utf-8-validate": "^5.0.4", 26 | "util": "^0.12.4", 27 | "zlib-sync": "^0.1.7" 28 | }, 29 | "devDependencies": { 30 | "eslint": "^7.21.0", 31 | "eslint-config-airbnb-base": "^14.2.1", 32 | "eslint-plugin-import": "^2.22.1", 33 | "nodemon": "^2.0.7" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/bot.js: -------------------------------------------------------------------------------- 1 | const launchTimestamp = Date.now(); 2 | 3 | const Discord = require('discord.js-light'); 4 | const Logger = require('./core/utils/logger'); 5 | const eventManager = require('./core/event-handler/event-handler'); 6 | const { token, intents } = require('./config/bot-config.json'); 7 | 8 | // Create the Discord client with the appropriate options 9 | const client = new Discord.Client({ 10 | disableMentions: 'everyone', 11 | messageCacheMaxSize: 30, 12 | messageEditHistoryMaxSize: 0, 13 | ws: { 14 | // List of intents for the bot, can be found at 15 | // https://discord.js.org/#/docs/main/stable/class/Intents?scrollTo=s-FLAGS 16 | intents, 17 | }, 18 | // Discord-js light caching options (modify for your needs) 19 | cacheGuilds: true, 20 | cacheChannels: true, 21 | cacheOverwrites: true, 22 | cacheRoles: true, 23 | cacheEmojis: false, 24 | cachePresences: false, 25 | }); 26 | 27 | eventManager.init(client, { launchTimestamp }); 28 | 29 | client.login(token) 30 | .then(() => Logger.info('Logged into Discord successfully')) 31 | .catch((err) => { 32 | Logger.error('Error logging into Discord', err); 33 | process.exit(); 34 | }); -------------------------------------------------------------------------------- /src/commands/dl.js: -------------------------------------------------------------------------------- 1 | //imports 2 | const { exec } = require('child_process'); 3 | const fs = require('fs'); 4 | const uuid = require('short-uuid'); 5 | const logger = require('../core/utils/logger'); 6 | const { MessageEmbed } = require('discord.js-light'); 7 | const colors = require('../core/graphics/colors'); 8 | const { findq } = require('../core/utils/qobuzhelper'); 9 | const { detect } = require('../core/utils/artisthelper'); 10 | const { Base64 } = require('js-base64'); 11 | const { chooseSeviceAccount } = require('../core/utils/serviceAccountHelper'); 12 | //configuration constants; EDIT HERE 13 | const REQUEST_CHANNEL = ''; //ID of channel where people request links 14 | const UPLOAD_CHANNEL = ''; //ID of channel where the bot will post the uploads 15 | const INDEX_URL = ''; //only needs to be specified if USE_INDEX is true, otherwise leave blank "". Make sure it has a trailing slash. 16 | const ENABLED_SERVICES = ['tidal', 'qobuz', 'deezer', 'soundcloud', 'spotify']; // to disable, remove from the list 17 | const BOT_DOWNLOADS_FOLDER = "/path/to/src/downloads"; // forward slashes, not backslashes for windows 18 | const RCLONE_REMOTE = chooseSeviceAccount(); //random via chooseSeviceAccount() or one remote just as string 19 | const USE_INDEX = true; //use google drive index or not, if set to true, make sure to specify INDEX_URL above, make sure to includd ":" at th end. 20 | const ALLOW_ARTISTS_PLAYLISTS = false; //wether you want to allow playlist/artists to be downloaded (can be hard on resources if set to true!) 21 | const SOUNDCLOUD_OAUTH_TOKEN = "OAuth xxxxxxx"; // soundcloud oAuth token. Only needs to be set if soundcloud is enabled, of course. 22 | const USE_BASE_64 = true; 23 | //end configuration constants 24 | 25 | function genb64link(link) { 26 | const url = Base64.encodeURI(link, { 27 | urlsafe: true 28 | }); 29 | return `https://links.gamesdrive.net/#/link/${url}.U2xhdiBNdXNpYyBCb3Q=`; 30 | } 31 | 32 | 33 | 34 | function isValidLink(link) { 35 | return link.startsWith('https://') || link.startsWith('http://'); 36 | } 37 | async function zipAndUpload(ctx, id, msg, path, remote, useIndex, link, callback) { 38 | const author = ctx.message.author; 39 | await msg.edit(`${author}\nZipping...`); 40 | await msg.react('📦'); 41 | exec(`7z a -mx0 -tzip ${path}/toUpload/${id}.zip ${path}/${id}/* `, (err, stdout, stderr) => { 42 | logger.info(`${stdout}`); 43 | 44 | if (err) { 45 | logger.error(`${err}`); 46 | ctx.message.reply(`*Error: Couldn't zip <${ctx.args[0]}>; ID: ${id}*`); 47 | msg.reactions.removeAll() 48 | fs.rmSync(`${BOT_DOWNLOADS_FOLDER}/${uploadID}`, { recursive: true }); 49 | return; 50 | 51 | } 52 | if (fs.statSync(`${path}/toUpload/${id}.zip`).size <= 1000) { 53 | msg.edit(`${author}\nError: Couldn't rip <${ctx.args[0]}> (Most likely: GEO restriction.)`); 54 | msg.reactions.removeAll(); 55 | ctx.message.react('❌'); 56 | ctx.message.react('🌐'); 57 | fs.rmSync(`${path}/toUpload/${id}.zip`); 58 | fs.rmSync(`${path}/${id}`, { recursive: true }, (err, stdout, stderr) => { logger.info(`Removed GEO restricted item.`); }); 59 | return; 60 | } 61 | if (!err) { 62 | exec(`rm -rf ${path}/${id}`, (err, stdout, stderr) => { 63 | if (err) { 64 | logger.error(`${err}`); 65 | msg.reactions.removeAll(); 66 | msg.edit(`${author}\nError: Couldn't remove temp folder.`); 67 | } 68 | if (!err) { 69 | msg.edit(`${author}\nUploading...`); 70 | msg.react("📤"); 71 | exec(`rclone copy ${path}/toUpload/${id}.zip ${remote} --ignore-existing --transfers 4 --checkers 8 --bwlimit 95M --contimeout 60s --timeout 300s --retries 3 --low-level-retries 10 --drive-chunk-size 128M --stats 1s -P -v `, (err, stdout, stderr) => { 72 | logger.info(stdout) 73 | if (err) { 74 | logger.error(`${err}`); 75 | msg.delete(`${author}\n*Error: Couldn't upload <${ctx.args[0]}>; ID: ${id}*`); 76 | msg.reactions.removeAll() 77 | fs.rmSync(`${BOT_DOWNLOADS_FOLDER}/${uploadID}`, { recursive: true }); 78 | fs.rmSync(`${BOT_DOWNLOADS_FOLDER}/toUpload/${uploadID}.zip`); 79 | return; 80 | } 81 | if (!err) { 82 | if (useIndex === true) { 83 | logger.info() 84 | let link; 85 | if (USE_BASE_64 == true) { 86 | link = genb64link(`${INDEX_URL}${id}.zip`); 87 | 88 | } else { 89 | link = stdout; 90 | } 91 | 92 | fs.rmSync(`${path}/toUpload/${id}.zip`); 93 | logger.info(link) 94 | msg.react('🔗') 95 | return callback(link); 96 | } 97 | if (useIndex === false) { 98 | msg.edit(`${author}\nGenerating link...`); 99 | msg.react('🔗') 100 | exec(`rclone link ${remote}${id}.zip`, (err, stdout, stderr) => { 101 | logger.info(stdout) 102 | let link; 103 | if (USE_BASE_64 == true) { 104 | link = genb64link(stdout); 105 | 106 | } else { 107 | link = stdout; 108 | } 109 | 110 | fs.rmSync(`${path}/toUpload/${id}.zip`); 111 | logger.info(link) 112 | return callback(link); 113 | }) 114 | 115 | fs.rmdirSync(`${path}/${id}`, { recursive: true, force: true }); 116 | 117 | 118 | } 119 | } 120 | 121 | }) 122 | } 123 | }); 124 | } 125 | }); 126 | } 127 | 128 | module.exports = { 129 | name: 'dl', 130 | aliases: ['download'], 131 | description: 'Downloads music', 132 | reqArgs: true, 133 | usage: '{ link }', 134 | exampleUsage: 'dl ', 135 | category: 'utility', 136 | cooldown: 1, 137 | async run(ctx) { 138 | logger.info(ctx.args) 139 | if (ctx.channel != REQUEST_CHANNEL) { 140 | ctx.message.reply(`This command can only be used in <#${REQUEST_CHANNEL}>`); 141 | } 142 | 143 | if (ctx.channel == REQUEST_CHANNEL) { 144 | let link = ctx.args[0]; 145 | let q = '--max-quality 4'; 146 | if (ctx.args[1]) { 147 | q = `--max-quality ${ctx.args[1]}` 148 | if (ctx.args[1] > 4) { 149 | q = '--max-quality 4'; 150 | } 151 | if (ctx.args[1] < 0) { 152 | q = '--max-quality 0'; 153 | } 154 | } 155 | 156 | 157 | const author = ctx.message.author; 158 | const uploadID = uuid.generate(); 159 | fs.mkdirSync(`${BOT_DOWNLOADS_FOLDER}/${uploadID}`); 160 | let service = ENABLED_SERVICES.find(service => link.includes(service)); 161 | if (!service && isValidLink(link)) { 162 | return ctx.channel.send(`Invalid service. Valid services: ${ENABLED_SERVICES.join(', ')}`); 163 | } 164 | let valid_link = await isValidLink(link); 165 | if (!valid_link) { 166 | return ctx.message.reply(`Invalid link.`); 167 | } 168 | if (link.includes('www.qobuz.com')) { 169 | await findq(link, (url) => { 170 | link = url 171 | }) 172 | } 173 | let msg = await ctx.message.reply(`Processing <${link}>...`); 174 | if (service == 'tidal' || service == 'deezer' || service == 'qobuz') { 175 | 176 | if (ALLOW_ARTISTS_PLAYLISTS == false) { 177 | detect(ctx.args[0], (r) => { 178 | if (r == true) { 179 | ctx.message.react('❌'); 180 | msg.edit(`**Downloading artists and playlists is not allowed.**`); 181 | return; 182 | } 183 | if (r == false) { 184 | msg.edit(`${author}\nRipping <${link}>...`); 185 | msg.react('⬇️'); 186 | exec(`rip url ${link} ${q} -i -d ${BOT_DOWNLOADS_FOLDER}/${uploadID}`, (err, stdout, stderr) => { 187 | if (err) { 188 | msg.reactions.removeAll() 189 | msg.edit(`${author}\n**Error: Couldn't download <${ctx.args[0]}>; ID: ${uploadID}** (possible reasons: region restrictions?, invalid link?)`); 190 | fs.rmSync(`${BOT_DOWNLOADS_FOLDER}/${uploadID}`, { recursive: true }); 191 | return; 192 | } 193 | if (!err) { 194 | 195 | zipAndUpload(ctx, uploadID, msg, BOT_DOWNLOADS_FOLDER, RCLONE_REMOTE, USE_INDEX, link, (drive_link) => { 196 | 197 | logger.info("uploaded") 198 | const embed = new MessageEmbed() 199 | .setColor(colors.info) 200 | .setTitle(`Download is finished`) 201 | .setDescription(`Your requested link, <${ctx.args[0]}>, is now available for download:`) 202 | .addField(`Download link`, drive_link) 203 | let uc = ctx.guild.channels.cache.find(channel => channel.id === UPLOAD_CHANNEL); 204 | let finished_msg = uc.send(embed); 205 | uc.send(`<@${author.id}>`); 206 | msg.edit(`${author}\nDone! You can find your link in <#${UPLOAD_CHANNEL}>`); 207 | ctx.message.react('✅'); 208 | msg.react('✅'); 209 | 210 | }); 211 | } 212 | }); 213 | } 214 | }) 215 | }; 216 | 217 | 218 | 219 | 220 | 221 | } 222 | if (service == 'soundcloud') { 223 | if (ALLOW_ARTISTS_PLAYLISTS == false) { 224 | detect(ctx.args[0], (r) => { 225 | if (r == true) { 226 | ctx.message.react('❌'); 227 | msg.edit(`**Downloading artists and playlists is not allowed.**`); 228 | return; 229 | } 230 | if (r == false) { 231 | msg.edit(`${author}\nRipping <${link}>...`); 232 | msg.react("⬇️") 233 | exec(`yt-dlp -x ${link} --output ${BOT_DOWNLOADS_FOLDER}/${uploadID}/"%(title)s.%(ext)s" --add-header Authorization:"${SOUNDCLOUD_OAUTH_TOKEN}" --add-metadata --embed-thumbnail`, (err, stdout, stderr) => { 234 | if (err) { 235 | logger.error(`${err}`); 236 | msg.reactions.removeAll() 237 | msg.edit(`${author}\n**Error: Couldn't download <${ctx.args[0]}>; ID: ${uploadID}**\n(possible reasons: region restrictions?, invalid link?)`); 238 | fs.rmSync(`${BOT_DOWNLOADS_FOLDER}/${uploadID}`, { recursive: true }); 239 | return; 240 | } 241 | if (!err) { 242 | zipAndUpload(ctx, uploadID, msg, BOT_DOWNLOADS_FOLDER, RCLONE_REMOTE, USE_INDEX, link, (drive_link) => { 243 | const embed = new MessageEmbed() 244 | .setColor(colors.info) 245 | .setTitle(`Download is finished`) 246 | .setDescription(`Your requested link, <${ctx.args[0]}>, is now available for download:`) 247 | .addField(`Download link`, drive_link) 248 | let uc = ctx.guild.channels.cache.find(channel => channel.id === UPLOAD_CHANNEL); 249 | let finished_msg = uc.send(embed); 250 | uc.send(`<@${author.id}>`); 251 | msg.edit(`${author}\nDone! You can find your link in <#${UPLOAD_CHANNEL}>`); 252 | ctx.message.react('✅'); 253 | msg.react('✅'); 254 | 255 | }); 256 | } 257 | }); 258 | } 259 | }) 260 | } 261 | 262 | } 263 | if (link.includes("spotify.com")) { 264 | if (link.includes("&utm_source=copy-link")) { 265 | link = link.replace("&utm_source=copy-link", ""); 266 | } 267 | if (ALLOW_ARTISTS_PLAYLISTS == false) { 268 | detect(ctx.args[0], (r) => { 269 | if (r == true) { 270 | ctx.message.react('❌'); 271 | msg.edit(`**Downloading artists and playlists is not allowed.**`); 272 | return; 273 | } 274 | if (r == false) { 275 | msg.edit(`${author}\nRipping <${link}>...`); 276 | msg.react('⬇️'); 277 | logger.info(`python ./src/core/utils/zspotify/ ${link} --root-path ${BOT_DOWNLOADS_FOLDER}/${uploadID}/`) 278 | exec(`python ./src/core/utils/zspotify/ ${link} --root-path ${BOT_DOWNLOADS_FOLDER}/${uploadID}/`, (err, stdout, stderr) => { 279 | if (err) { 280 | logger.error(`${err}`); 281 | msg.edit(`**Error: Couldn't download <${ctx.args[0]}>; ID: ${uploadID}**\n(possible reasons: region restrictions?, invalid link?)\n *Note: This issue might be exclusive to spotify, if this item is available on another service, try there.*`); 282 | fs.rmSync(`${BOT_DOWNLOADS_FOLDER}/${uploadID}`, { recursive: true }); 283 | return; 284 | } 285 | if (!err) { 286 | logger.info(stdout) 287 | zipAndUpload(ctx, uploadID, msg, BOT_DOWNLOADS_FOLDER, RCLONE_REMOTE, USE_INDEX, link, (drive_link) => { 288 | logger.info(drive_link) 289 | const embed = new MessageEmbed() 290 | .setColor(colors.info) 291 | .setTitle(`Download is finished`) 292 | .setDescription(`Your requested link, <${ctx.args[0]}>, is now available for download:`) 293 | .addField(`Download link`, drive_link) 294 | let uc = ctx.guild.channels.cache.find(channel => channel.id === UPLOAD_CHANNEL); 295 | let finished_msg = uc.send(embed); 296 | uc.send(`<@${author.id}>`); 297 | msg.edit(`${author}\nDone! You can find your link in <#${UPLOAD_CHANNEL}>`); 298 | ctx.message.react('✅'); 299 | msg.react('✅'); 300 | }); 301 | } 302 | }) 303 | } 304 | }); 305 | 306 | } 307 | } 308 | } 309 | 310 | } 311 | 312 | }; -------------------------------------------------------------------------------- /src/config/bot-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Music Slav", 3 | "status": "All slavs 👀", 4 | "statusType": "WATCHING", 5 | "token": "", 6 | "intents": [ 7 | "GUILDS", 8 | "GUILD_MESSAGES" 9 | ] 10 | } -------------------------------------------------------------------------------- /src/config/commands-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "prefix": "!" 3 | } -------------------------------------------------------------------------------- /src/config/logging-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "commands": true, 3 | "guildJoin": true, 4 | "guildLeave": true 5 | } -------------------------------------------------------------------------------- /src/core/command-handler/Ctx.js: -------------------------------------------------------------------------------- 1 | const colors = require('../graphics/colors'); 2 | 3 | module.exports = class Ctx { 4 | constructor(message, commandName, args) { 5 | this.message = message; 6 | this.channel = message.channel; 7 | this.guild = message.guild; 8 | 9 | this.user = message.author; 10 | this.member = message.member; 11 | 12 | this.commandName = commandName; 13 | this.args = args; 14 | } 15 | 16 | async react(reaction) { 17 | await this.message.react(reaction); 18 | } 19 | 20 | async sendText(text, options = {}) { 21 | await this.channel.send(text, options); 22 | } 23 | 24 | async sendEmbed(embed, options = {}) { 25 | await this.channel.send(embed, options); 26 | } 27 | 28 | async sendTextAndEmbed(text, embed, options = {}) { 29 | options.embed = embed; 30 | await this.channel.send(text, options); 31 | } 32 | 33 | get hexColor() { 34 | return this.guild ? this.guild.me.displayHexColor : colors.primary; 35 | } 36 | 37 | get guildID() { 38 | return this.guild ? this.guild.id : null; 39 | } 40 | 41 | get userID() { 42 | return this.user.id; 43 | } 44 | 45 | get channelID() { 46 | return this.channel.id; 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/core/command-handler/command-handler.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Discord = require('discord.js-light'); 4 | const Logger = require('../utils/logger'); 5 | 6 | const commands = new Discord.Collection(); 7 | 8 | function registerCommands(dir) { 9 | fs.readdir(dir, (e, files) => { 10 | if (e) { 11 | throw e; 12 | } 13 | 14 | files.forEach((file) => { 15 | const filepath = path.resolve(dir, file); 16 | 17 | fs.stat(filepath, (_, stats) => { 18 | if (stats.isDirectory()) { 19 | registerCommands(filepath); 20 | } else if (stats.isFile() && file.endsWith('.js')) { 21 | const command = require(filepath); 22 | 23 | if (commands.has(command.name.toLowerCase())) { 24 | Logger.error(`Command name duplicate: ${command.name}`); 25 | process.exit(); 26 | } 27 | if (commands.some((cmd) => cmd.aliases.some((a) => command.aliases.includes(a)))) { 28 | Logger.error(`Command alias duplicate: ${command.aliases}`); 29 | process.exit(); 30 | } 31 | 32 | commands.set(command.name.toLowerCase(), command); 33 | } 34 | }); 35 | }); 36 | }); 37 | } 38 | 39 | registerCommands(path.join(__dirname, '../', '../', 'commands')); 40 | 41 | function traverse(dir, filename) { 42 | for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) { 43 | const direntPath = path.join(dir, dirent.name); 44 | 45 | if (dirent.isDirectory()) { 46 | const result = traverse(direntPath, filename); 47 | if (result) { 48 | return result; 49 | } 50 | } else if (dirent.name === `${filename}.js`) { 51 | return direntPath; 52 | } 53 | } 54 | return null; 55 | } 56 | 57 | module.exports = { 58 | getCommand(commandName) { 59 | return commands.get(commandName.toLowerCase()) 60 | || commands.find((cmd) => cmd.aliases && cmd.aliases.includes(commandName.toLowerCase())); 61 | }, 62 | 63 | getCommandsByCategory(category) { 64 | return commands.filter((cmd) => cmd.category.toLowerCase() === category.toLowerCase()); 65 | }, 66 | 67 | getAllCommands() { 68 | return commands; 69 | }, 70 | 71 | reloadCommand(commandName) { 72 | const command = this.getCommand(commandName); 73 | 74 | if (!command) { 75 | return `There is no command with name or alias \`${commandName}\`.`; 76 | } 77 | 78 | const commandPath = traverse(path.join(__dirname, '../../commands'), command.name); 79 | if (!commandPath) { 80 | return 'File not found'; 81 | } 82 | 83 | delete require.cache[require.resolve(commandPath)]; 84 | 85 | try { 86 | const newCommand = require(commandPath); 87 | commands.set(newCommand.name.toLowerCase(), newCommand); 88 | return `Command \`${command.name}\` successfully reloaded!`; 89 | } catch (err) { 90 | return `There was an error while reloading a command \`${command.name}\`:\n\`${err}\``; 91 | } 92 | }, 93 | 94 | async getCommandData(message, prefix) { 95 | const msgContent = message.content; 96 | let command; 97 | let args; 98 | 99 | if (msgContent.trim().toLowerCase() === `<@!${message.client.user.id}>` 100 | || msgContent.trim().toLowerCase() === `<@${message.client.user.id}>`) { 101 | command = commands.get('mention'); 102 | } 103 | 104 | if (command) { 105 | args = msgContent.slice(2 + command.name.length).split(/ +/); 106 | args.shift(); 107 | } 108 | 109 | if (!command) { 110 | if (msgContent.startsWith(`<@!${message.client.user.id}>` || `<@${message.client.user.id}>`)) { 111 | args = msgContent.slice(23).split(/ +/); 112 | } else if (msgContent.toLowerCase().startsWith(prefix)) { 113 | args = msgContent.slice(prefix.length).split(/ +/); 114 | } 115 | 116 | if (args) { 117 | const commandName = args.shift().toLowerCase(); 118 | command = commands.get(commandName) 119 | || commands.find((cmd) => cmd.aliases && cmd.aliases.includes(commandName)); 120 | } 121 | } 122 | 123 | return { command, args }; 124 | }, 125 | 126 | async runCommand(ctx) { 127 | const command = commands.get(ctx.commandName.toLowerCase()) 128 | || commands.find((cmd) => cmd.aliases && cmd.aliases.includes(ctx.commandName.toLowerCase())); 129 | try { 130 | await command.run(ctx); 131 | Logger.command(ctx); 132 | return null; 133 | } catch (err) { 134 | Logger.error(`Error while executing the command ${ctx.commandName}`, err); 135 | return err; 136 | } 137 | }, 138 | }; 139 | -------------------------------------------------------------------------------- /src/core/discord-utils/permissions-handler.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | checkTextPerms(channel) { 3 | return Object.entries(this.textChannelPerms) 4 | .filter((p) => !channel.permissionsFor(channel.guild.me.id).has(p[0])) 5 | .map((p) => p[1]); 6 | }, 7 | 8 | checkVoicePerms(vc) { 9 | return Object.entries(this.voiceChannelPerms) 10 | .filter((p) => !vc.permissionsFor(vc.guild.me.id).has(p[0])) 11 | .map((p) => p[1]); 12 | }, 13 | 14 | checkGuildPerms(guild) { 15 | return Object.entries(this.guildPerms) 16 | .filter((p) => !guild.me.hasPermission(p[0])) 17 | .map((p) => p[1]); 18 | }, 19 | 20 | textChannelPerms: { 21 | VIEW_CHANNEL: 'Read Messages', 22 | SEND_MESSAGES: 'Send Messages', 23 | EMBED_LINKS: 'Embed Links', 24 | ADD_REACTIONS: 'Add Reactions', 25 | USE_EXTERNAL_EMOJIS: 'Use External Emojis', 26 | ATTACH_FILES: 'Attach Files', 27 | }, 28 | 29 | guildPerms: { 30 | MANAGE_CHANNELS: 'Manage Channels', 31 | MANAGE_ROLES: 'Manage Roles', 32 | MOVE_MEMBERS: 'Move Members', 33 | CREATE_INSTANT_INVITE: 'Create Invite', 34 | }, 35 | 36 | voiceChannelPerms: { 37 | VIEW_CHANNEL: 'View Channel', 38 | CONNECT: 'Connect', 39 | SPEAK: 'Speak', 40 | USE_VAD: 'Use Voice Activity', 41 | }, 42 | }; -------------------------------------------------------------------------------- /src/core/event-handler/event-handler.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const RequireAll = require('require-all'); 3 | const Logger = require('../utils/logger'); 4 | const { status, statusType } = require('../../config/bot-config.json'); 5 | 6 | let discordClient; 7 | 8 | module.exports = { 9 | init(client, args = { launchTimestamp: Date.now() }) { 10 | discordClient = client; 11 | this.initEssentialEvents(client, args); 12 | this.initEvents(client); 13 | }, 14 | 15 | initEssentialEvents(client = discordClient, args = { launchTimestamp: Date.now() }) { 16 | // Ready event, which gets fired only once when the bot reaches the ready state 17 | client.once('ready', async () => { 18 | const updateBotStatus = async () => { 19 | await client.user.setActivity(status, { type: statusType }); 20 | }; 21 | 22 | await updateBotStatus(); 23 | // Updates the bot status every hour 24 | client.setInterval(() => updateBotStatus(), 3600000); 25 | 26 | Logger.info(`Successfully launched in ${(Date.now() - args.launchTimestamp) / 1000} seconds!`); 27 | }); 28 | 29 | /* Raw listener for messageReactionAdd event 30 | Uncomment the following code if you need your bot 31 | to listen to messageReactionAdd events on uncached messages 32 | */ 33 | /* 34 | client.on('raw', async (event) => { 35 | // Listen only to reactionAdd events 36 | if (event.t !== 'MESSAGE_REACTION_ADD') { 37 | return; 38 | } 39 | 40 | const { d: data } = event; 41 | if (typeof client.channels.cache.get(data.channel_id) === 'undefined') { 42 | return; 43 | } 44 | 45 | const channel = await client.channels.fetch(data.channel_id); 46 | 47 | // if the message is already in the cache, don't re-emit the event 48 | if (channel.messages.cache.has(data.message_id)) { 49 | return; 50 | } 51 | 52 | const user = await client.users.fetch(data.user_id); 53 | const message = await channel.messages.fetch(data.message_id); 54 | // Custom emoji are keyed by IDs, while unicode emoji are keyed by names 55 | const reaction = message.reactions.resolve(data.emoji.id || data.emoji.name); 56 | 57 | client.emit('messageReactionAdd', reaction, user); 58 | }); 59 | */ 60 | 61 | // Some other somewhat important events that the bot should listen to 62 | client.on('error', (err) => Logger.error('The client threw an error', err)); 63 | 64 | client.on('shardError', (err) => Logger.error('A shard threw an error', err)); 65 | 66 | client.on('warn', (warn) => Logger.warn('The client received a warning', warn)); 67 | }, 68 | 69 | initEvents(client = discordClient) { 70 | // Gets all the events from the events folder 71 | const events = Object.entries( 72 | RequireAll({ 73 | dirname: path.join(__dirname, 'events'), 74 | filter: /^(?!-)(.+)\.js$/, 75 | }), 76 | ); 77 | 78 | /* 79 | Binds the events gotten with the code above to the client: 80 | e[0] is the event name (the name of the file) 81 | e[1] is the function that will get executed when the event gets fired 82 | */ 83 | events.forEach((e) => client.on(e[0], e[1].bind(null, client))); 84 | }, 85 | }; 86 | -------------------------------------------------------------------------------- /src/core/event-handler/events/message.js: -------------------------------------------------------------------------------- 1 | const commandHandler = require('../../command-handler/command-handler'); 2 | const permissionsHandler = require('../../discord-utils/permissions-handler'); 3 | const embeds = require('../../graphics/embeds'); 4 | const Logger = require('../../utils/logger'); 5 | const Ctx = require('../../command-handler/Ctx'); 6 | const commandConfigs = require('../../../config/commands-config.json'); 7 | 8 | module.exports = async(client, message) => { 9 | try { 10 | if (message.author.bot) { 11 | return; 12 | } 13 | 14 | if (!message.guild) { 15 | return; 16 | } 17 | 18 | const { guild, channel, member } = message; 19 | 20 | 21 | const commandCheck = await commandHandler.getCommandData(message, commandConfigs.prefix); 22 | if (!commandCheck.command) { 23 | return; 24 | } 25 | 26 | const { command, args } = commandCheck; 27 | 28 | if (command.reqArgs && !args.length) { 29 | await channel.send(embeds.error('The command is missing some arguments')); 30 | return; 31 | } 32 | 33 | const missingTextPerms = permissionsHandler.checkTextPerms(channel); 34 | if (missingTextPerms.length > 0) { 35 | await channel.send(embeds.error(`Missing text channel permissions:\n- ${missingTextPerms.join('\n- ')}`)); 36 | return; 37 | } 38 | 39 | const missingGuildPerms = permissionsHandler.checkGuildPerms(guild); 40 | if (missingGuildPerms.length > 0) { 41 | await channel.send(embeds.error(`Missing server permissions:\n- ${missingGuildPerms.join('\n- ')}`)); 42 | return; 43 | } 44 | 45 | 46 | const ctx = new Ctx(message, command.name, args); 47 | 48 | const error = await commandHandler.runCommand(ctx); 49 | Logger.command(ctx); 50 | if (error) { 51 | await channel.send(embeds.error(`Error while executing the command ${command.name}`)); 52 | Logger.error(`Error caused by the ${ctx.commandName} command`, error); 53 | } 54 | } catch (e) { 55 | Logger.error('Error from message event', e); 56 | } 57 | }; -------------------------------------------------------------------------------- /src/core/graphics/colors.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | primary: '#000', 3 | info: '#43b581', 4 | error: '#faa619', 5 | warn: '#f14846', 6 | }; 7 | -------------------------------------------------------------------------------- /src/core/graphics/embeds.js: -------------------------------------------------------------------------------- 1 | const { MessageEmbed } = require('discord.js-light'); 2 | const colors = require('./colors'); 3 | const { prefix } = require('../../config/commands-config.json'); 4 | 5 | module.exports = { 6 | info(color, description) { 7 | return new MessageEmbed() 8 | .setColor(color || colors.primary) 9 | .setTitle('Info') 10 | .setDescription(description) 11 | }, 12 | 13 | warn(description) { 14 | return new MessageEmbed() 15 | .setColor(colors.warn) 16 | .setTitle('Warning') 17 | .setDescription(description) 18 | }, 19 | 20 | error(description) { 21 | return new MessageEmbed() 22 | .setColor(colors.error) 23 | .setTitle('Error') 24 | .setDescription(description) 25 | } 26 | }; -------------------------------------------------------------------------------- /src/core/utils/README.md: -------------------------------------------------------------------------------- 1 | you need a copy of zspotify in here. It's not included since this project is hosted on github. 2 | 3 | important: 4 | change line 37 - 39 in zspotify.py from: 5 | ``` 6 | while len(user_name) == 0: 7 | user_name = input('Username: ') 8 | password = getpass() 9 | ``` 10 | to: 11 | ``` 12 | while len(user_name) == 0: 13 | user_name = "" 14 | password = "" 15 | ``` -------------------------------------------------------------------------------- /src/core/utils/artisthelper.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const cheerio = require('cheerio'); 3 | 4 | module.exports = { 5 | async detect(link, callback) { 6 | switch (true) { 7 | case link.includes("tidal"): 8 | if (link.includes("artist") || link.includes("playlist")) { 9 | return callback(true); 10 | } else { 11 | return callback(false) 12 | } 13 | break; 14 | case link.includes("www.qobuz.com"): 15 | axios.get(link).then(response => { 16 | const $ = cheerio.load(response.data); 17 | if ($('body').attr('context') == "artist" || $('body').attr('context') == "playlist_show") { 18 | return callback(true); 19 | } else { 20 | return callback(false); 21 | } 22 | }) 23 | break; 24 | case link.includes("play.qobuz.com") || link.includes("open.qobuz.com"): 25 | if (link.includes("artist") || link.includes("playlist")) { 26 | return callback(true); 27 | } else { 28 | return callback(false) 29 | } 30 | break; 31 | case link.includes("deezer.com"): 32 | let cleaned_link = link.split("#")[0]; 33 | if (cleaned_link.includes("artist") || cleaned_link.includes("playlist")) { 34 | return callback(true); 35 | } else { 36 | return callback(false) 37 | } 38 | break; 39 | case link.includes("soundcloud.com"): 40 | let splitArr = link.split("/"); 41 | if (splitArr.length > 5) { 42 | return callback(true); 43 | } 44 | if (splitArr.length < 5) { 45 | return callback(true); 46 | } 47 | if (splitArr.length == 5) { 48 | return callback(false); 49 | } 50 | break; 51 | case link.includes("spotify.com"): 52 | if (link.includes("artist") || link.includes("playlist")) { 53 | return callback(true); 54 | } else { 55 | return callback(false) 56 | } 57 | break; 58 | 59 | 60 | } 61 | 62 | } 63 | } -------------------------------------------------------------------------------- /src/core/utils/logger.js: -------------------------------------------------------------------------------- 1 | const beautify = require('beautify.log').default; 2 | const { name } = require('../../config/bot-config.json'); 3 | const { commands, guildJoin, guildLeave } = require('../../config/logging-config.json'); 4 | 5 | function formatText(text) { 6 | return text.replace('BOTNAME', name); 7 | } 8 | 9 | module.exports = { 10 | info(text) { 11 | const prefix = '{fgGreen}[BOTNAME - INFO] {reset}'; 12 | beautify.log(formatText(prefix + text)); 13 | }, 14 | 15 | warn(text, warn) { 16 | const prefix = '{fgYellow}[BOTNAME - WARN] {reset}'; 17 | beautify.log(formatText(`${prefix + text}\n${warn}`)); 18 | }, 19 | 20 | error(text, err) { 21 | const prefix = '{fgRed}[BOTNAME - ERROR] {reset}'; 22 | beautify.log(formatText(`${prefix + text}\n${err}`)); 23 | }, 24 | 25 | command(ctx) { 26 | if (commands) { 27 | const prefix = '{fgMagenta}[BOTNAME - COMMAND] {reset}'; 28 | beautify.log(formatText(`${prefix}${ctx.member.displayName} (${ctx.member.id}) used ${ctx.commandName} in ${ctx.guild.name} (${ctx.guild.id})`)); 29 | } 30 | }, 31 | 32 | guildJoin(guild) { 33 | if (guildJoin) { 34 | const prefix = '{fgCyan}[BOTNAME - GUILD JOIN] {reset}'; 35 | beautify.log(formatText(`${prefix}Joined (${guild.name}) (${guild.id}), members: ${guild.memberCount}`)); 36 | } 37 | }, 38 | 39 | guildLeave(guild) { 40 | if (guildLeave) { 41 | const prefix = '{fgYellow}[BOTNAME - GUILD LEAVE] {reset}'; 42 | beautify.log(formatText(`${prefix}Left (${guild.name}) (${guild.id}), members: ${guild.memberCount}`)); 43 | } 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/core/utils/qobuzhelper.js: -------------------------------------------------------------------------------- 1 | const cheerio = require('cheerio'); 2 | const axios = require('axios'); 3 | 4 | module.exports = { 5 | async findq(link, callback) { 6 | axios.get(link).then(response => { 7 | const $ = cheerio.load(response.data); 8 | try { 9 | const title = $('.album-meta__title').text(); 10 | const artist = $('.album-meta__artist').text(); 11 | const res = axios.get(`https://www.qobuz.com/api.json/0.2/catalog/search`, { 12 | headers: { 13 | 'X-App-Id': '', // qobuz X-App-Id Header 14 | 'X-User-Auth-Token': '', //qobuz X-User-Auth-Token Header 15 | }, 16 | params: { query: `${title} ${artist}`, offset: '0', limit: '10' }, 17 | }).then(res => { 18 | let albums = res.data.albums.items; 19 | albums.forEach(album => { 20 | if (album.title == title && album.artist.name == artist) { 21 | let album_id = album.id; 22 | let url = `https://play.qobuz.com/album/${album_id}` 23 | return callback(url); 24 | } else { 25 | return callback(link) 26 | } 27 | }) 28 | }) 29 | } catch (err) { 30 | return err; 31 | } 32 | 33 | }) 34 | 35 | 36 | 37 | } 38 | } -------------------------------------------------------------------------------- /src/core/utils/serviceAccountHelper.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | chooseSeviceAccount() { 3 | let randInt = Math.floor(Math.random() * (100 - 1) + 1); 4 | let account; 5 | if (randInt < 10) { 6 | account = 'dst' + '00' + String(randInt) + ':'; 7 | } 8 | if (randInt >= 10) { 9 | account = 'dst' + '0' + String(randInt) + ':'; 10 | } 11 | return account; 12 | } 13 | } -------------------------------------------------------------------------------- /src/core/utils/zspotify/zspotify_source_in_here.txt: -------------------------------------------------------------------------------- 1 | find it yourself! -------------------------------------------------------------------------------- /src/downloads/toUpload/example.zip: -------------------------------------------------------------------------------- 1 | //zip files that are being uploaded. -------------------------------------------------------------------------------- /src/sharding.js: -------------------------------------------------------------------------------- 1 | const { ShardingManager } = require('discord.js-light'); 2 | const { token } = require('./config/bot-config.json'); 3 | const Logger = require('./core/utils/logger'); 4 | 5 | const manager = new ShardingManager('./src/bot.js', { token }); 6 | 7 | manager.on('shardCreate', (shard) => Logger.info(`Launched shard n° ${shard.id}`)); 8 | manager.spawn(); 9 | -------------------------------------------------------------------------------- /zspotify-reqs.txt: -------------------------------------------------------------------------------- 1 | ffmpy 2 | https://github.com/kokarare1212/librespot-python/archive/refs/heads/rewrite.zip 3 | music_tag 4 | Pillow 5 | protobuf 6 | tabulate 7 | tqdm --------------------------------------------------------------------------------