diff --git a/client/web/client.js b/client/web/client.js index a5fb14f..7b88d3d 100644 --- a/client/web/client.js +++ b/client/web/client.js @@ -93,39 +93,68 @@ document.getElementById('uploadForm').addEventListener('submit', function(event) var fileInput = document.getElementById('myFile'); var file = fileInput.files[0]; + var youtubeLink = document.getElementById('youtubeLink').value; - var objectURL = URL.createObjectURL(file); - var audio = new Audio(objectURL); + if (file) { + var objectURL = URL.createObjectURL(file); + var audio = new Audio(objectURL); - audio.addEventListener('loadedmetadata', function() { - var duration = audio.duration; - console.log(duration); + audio.addEventListener('loadedmetadata', function() { + var duration = audio.duration; + console.log(duration); - if (duration > 10) { - alert('File is longer than 10 seconds.'); - return; - } + if (duration > 10) { + alert('File is longer than 10 seconds.'); + return; + } - if (file.size > 1024 * 1024) { - alert('File is larger than 1MB.'); - return; - } + if (file.size > 1024 * 1024) { + alert('File is larger than 1MB.'); + return; + } - if (file.name.split('.').pop().toLowerCase() !== 'mp3') { - alert('Only .mp3 files are allowed.'); - return; - } + if (file.name.split('.').pop().toLowerCase() !== 'mp3') { + alert('Only .mp3 files are allowed.'); + return; + } - var formData = new FormData(); - formData.append('myFile', file); + var formData = new FormData(); + formData.append('myFile', file); - fetch('/upload', { + fetch('/upload', { + method: 'POST', + body: formData + }) + .then(response => response.text()) + .then(data => { + console.log(data); + alert('File uploaded successfully.'); + + // Call loadFiles again to update the file list + loadFiles(); + }) + .catch(error => { + console.error(error); + alert('An error occurred while uploading the file.'); + }); + }); + } else if (youtubeLink) { + console.log(youtubeLink); + fetch('/upload-youtube', { method: 'POST', - body: formData + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ url: youtubeLink }), }) .then(response => response.text()) .then(data => { console.log(data); + // if response is not ok, alert + if (data !== 'ok') { + alert(data); + return; + } alert('File uploaded successfully.'); // Call loadFiles again to update the file list @@ -135,5 +164,7 @@ document.getElementById('uploadForm').addEventListener('submit', function(event) console.error(error); alert('An error occurred while uploading the file.'); }); - }); + } else { + alert('Please select a file or paste a YouTube link.'); + } }); diff --git a/client/web/index.html b/client/web/index.html index ea7065c..1e59196 100644 --- a/client/web/index.html +++ b/client/web/index.html @@ -19,6 +19,7 @@ + diff --git a/client/webserver.ts b/client/webserver.ts index 909624c..c7a6b29 100644 --- a/client/webserver.ts +++ b/client/webserver.ts @@ -3,6 +3,10 @@ import express from 'express'; import multer, { diskStorage } from 'multer'; import path from 'path'; import { LoggerColors } from '../bot'; +import ytdl from 'ytdl-core'; +import fs from 'fs'; +import bodyParser from 'body-parser'; +import ffmpeg from 'fluent-ffmpeg'; const app = express(); const storage = diskStorage({ @@ -12,6 +16,7 @@ const storage = diskStorage({ } }); +app.use(bodyParser.json()); const upload = multer({ storage: storage, @@ -20,6 +25,7 @@ const upload = multer({ if (path.extname(file.originalname) !== '.mp3') { return cb(new Error('Only .mp3 files are allowed')); } + cb(null, true); } }); @@ -32,6 +38,55 @@ app.post('/upload', upload.single('myFile'), async (req, res) => { res.send('File uploaded successfully.'); }); +app.post('/upload-youtube', async (req, res) => { + const url = req.body.url; + + if (ytdl.validateURL(url)) { + const info = await ytdl.getInfo(url); + // remove special characters from the title and white spaces + const title = info.videoDetails.title.replace(/[^a-zA-Z ]/g, "").replace(/\s+/g, '-').toLowerCase(); + + // Create a temporary directory to store the uploaded file so validation can be done + const tempDir = fs.mkdtempSync('temp'); + const outputFilePath = path.resolve(tempDir, Date.now() + '-' + title + '.mp3'); + + const videoReadableStream = ytdl(url, { filter: 'audioonly' }); + const fileWritableStream = fs.createWriteStream(outputFilePath); + + videoReadableStream.pipe(fileWritableStream); + + fileWritableStream.on('finish', () => { + ffmpeg.ffprobe(outputFilePath, function(err, metadata) { + if (err) { + fs.rmSync(tempDir, { recursive: true, force: true }); + return res.status(500).send('Error occurred during processing.'); + } + const duration = metadata.format.duration; + + if (duration == undefined) { + fs.rmSync(tempDir, { recursive: true, force: true }); + return res.status(400).send('Something went wrong.'); + } + if (duration > 10) { + fs.rmSync(tempDir, { recursive: true, force: true }); + return res.status(400).send('File is longer than 10 seconds.'); + } else { + // Move the file from the temporary directory to its final destination + const finalFilePath = path.resolve(__dirname, '../sounds/', Date.now() + '-' + title + '.mp3'); + fs.renameSync(outputFilePath, finalFilePath); + + res.send('File uploaded successfully.'); + } + + // Remove the temporary directory and its contents once done + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + }); + } else { + res.status(400).send('Invalid url provided.'); + } +}); + // create a enpoint to return a file from the sounds folder (use the file name) with as little code as possible app.use('/sounds', express.static(path.join(__dirname, '../sounds'))); diff --git a/package-lock.json b/package-lock.json index 015e4d3..9fc0da5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,16 +10,21 @@ "license": "ISC", "dependencies": { "@discordjs/voice": "~0.16.0", + "@types/fluent-ffmpeg": "^2.1.22", "@types/multer": "~1.4.8", "@types/node": "~20.8.2", + "body-parser": "^1.20.2", "discord.js": "~14.13.0", "dotenv": "~16.3.1", "express": "~4.18.2", + "fluent-ffmpeg": "^2.1.2", + "libsodium-wrappers": "~0.7.13", "multer": "~1.4.5-lts.1", - "node-schedule": "^2.1.1", + "node-schedule": "~2.1.1", "sodium": "~3.0.2", "ts-node": "~10.9.1", - "typescript": "~5.2.2" + "typescript": "~5.2.2", + "ytdl-core": "^4.11.5" }, "devDependencies": { "@types/express": "~4.17.18", @@ -225,9 +230,9 @@ } }, "node_modules/@types/express": { - "version": "4.17.18", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz", - "integrity": "sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==", + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.19.tgz", + "integrity": "sha512-UtOfBtzN9OvpZPPbnnYunfjM7XCI4jyk1NvnFhTVz5krYAnW4o5DCoIekvms+8ApqhB4+9wSge1kBijdfTSmfg==", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -246,6 +251,14 @@ "@types/send": "*" } }, + "node_modules/@types/fluent-ffmpeg": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.22.tgz", + "integrity": "sha512-ZZPDDrDOb2Ahp5fxZzuw64f0rCcviv+SDuCyJ1PIF/UFn9wNHtb/bY8Dj/2nrbQ7SNsGI7gaO2wJVkkU2HBcMg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", @@ -265,9 +278,12 @@ } }, "node_modules/@types/node": { - "version": "20.8.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", - "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==" + "version": "20.8.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.6.tgz", + "integrity": "sha512-eWO4K2Ji70QzKUqRy6oyJWUeB7+g2cRagT3T/nxYibYcT4y2BDL8lqolRXjTHmkZCdJfIPaY73KbJAZmcryxTQ==", + "dependencies": { + "undici-types": "~5.25.1" + } }, "node_modules/@types/node-schedule": { "version": "2.1.1", @@ -370,13 +386,18 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -384,7 +405,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -636,6 +657,43 @@ "node": ">= 0.10.0" } }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -658,6 +716,18 @@ "node": ">= 0.8" } }, + "node_modules/fluent-ffmpeg": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz", + "integrity": "sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==", + "dependencies": { + "async": ">=0.2.9", + "which": "^1.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -767,6 +837,24 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/libsodium": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.13.tgz", + "integrity": "sha512-mK8ju0fnrKXXfleL53vtp9xiPq5hKM0zbDQtcxQIsSmxNgSxqCj6R7Hl9PkrNe2j29T4yoDaF7DJLK9/i5iWUw==" + }, + "node_modules/libsodium-wrappers": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.13.tgz", + "integrity": "sha512-kasvDsEi/r1fMzKouIDv7B8I6vNmknXwGiYodErGuESoFTohGSKZplFtVxZqHaoQ217AynyIFgnOVRitpHs0Qw==", + "dependencies": { + "libsodium": "^0.7.13" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -790,6 +878,18 @@ "node": ">=12" } }, + "node_modules/m3u8stream": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.6.tgz", + "integrity": "sha512-LZj8kIVf9KCphiHmH7sbFQTVe4tOemb202fWwvJwR9W5ENW/1hxJN6ksAWGhQgSBSa3jyWhnjKU1Fw1GaOdbyA==", + "dependencies": { + "miniget": "^4.2.2", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/magic-bytes.js": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.5.0.tgz", @@ -851,6 +951,14 @@ "node": ">= 0.6" } }, + "node_modules/miniget": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/miniget/-/miniget-4.2.3.tgz", + "integrity": "sha512-SjbDPDICJ1zT+ZvQwK0hUcRY4wxlhhNpHL9nJOB2MEAXRGagTljsO8MEDzQMTFf0Q8g4QNi8P9lEm/g7e+qgzA==", + "engines": { + "node": ">=12" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -1023,9 +1131,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -1079,6 +1187,11 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, "node_modules/send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -1282,6 +1395,11 @@ "node": ">=14.0" } }, + "node_modules/undici-types": { + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", + "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1316,6 +1434,17 @@ "node": ">= 0.8" } }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/ws": { "version": "8.14.2", "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", @@ -1351,6 +1480,19 @@ "engines": { "node": ">=6" } + }, + "node_modules/ytdl-core": { + "version": "4.11.5", + "resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.11.5.tgz", + "integrity": "sha512-27LwsW4n4nyNviRCO1hmr8Wr5J1wLLMawHCQvH8Fk0hiRqrxuIu028WzbJetiYH28K8XDbeinYW4/wcHQD1EXA==", + "dependencies": { + "m3u8stream": "^0.8.6", + "miniget": "^4.2.2", + "sax": "^1.1.3" + }, + "engines": { + "node": ">=12" + } } } } diff --git a/package.json b/package.json index cdc5bbc..2b30f24 100644 --- a/package.json +++ b/package.json @@ -11,17 +11,21 @@ "license": "ISC", "dependencies": { "@discordjs/voice": "~0.16.0", + "@types/fluent-ffmpeg": "^2.1.22", "@types/multer": "~1.4.8", "@types/node": "~20.8.2", + "body-parser": "^1.20.2", "discord.js": "~14.13.0", "dotenv": "~16.3.1", "express": "~4.18.2", + "fluent-ffmpeg": "^2.1.2", + "libsodium-wrappers": "~0.7.13", "multer": "~1.4.5-lts.1", "node-schedule": "~2.1.1", - "libsodium-wrappers": "~0.7.13", "sodium": "~3.0.2", "ts-node": "~10.9.1", - "typescript": "~5.2.2" + "typescript": "~5.2.2", + "ytdl-core": "^4.11.5" }, "devDependencies": { "@types/express": "~4.17.18",