diff --git a/.gitignore b/.gitignore index 3be286c..c9b42e3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ .nx/cache .nx +uploads + .idea/ ### Node ### # Logs @@ -146,4 +148,4 @@ dist # End of https://www.toptal.com/developers/gitignore/api/node -**/vite.config.{js,ts,mjs,mts,cjs,cts}.timestamp* \ No newline at end of file +**/vite.config.{js,ts,mjs,mts,cjs,cts}.timestamp* diff --git a/apps/audio-file-uploader/examples/CreateTrack.http b/apps/audio-file-uploader/examples/CreateTrack.http new file mode 100644 index 0000000..ef78a16 --- /dev/null +++ b/apps/audio-file-uploader/examples/CreateTrack.http @@ -0,0 +1,7 @@ +### +POST localhost:3001/api/upload/audio/from-youtube +Content-Type: application/json + +{ + "urls": ["https://www.youtube.com/watch?v=nkvhwh7CQqA&ab_channel=ForsakenTunes"] +} diff --git a/apps/audio-file-uploader/src/app/app.controller.ts b/apps/audio-file-uploader/src/app/app.controller.ts index 618c116..f6fe0f6 100644 --- a/apps/audio-file-uploader/src/app/app.controller.ts +++ b/apps/audio-file-uploader/src/app/app.controller.ts @@ -1,8 +1,10 @@ -import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common'; +import { Body, Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { Express } from 'express'; import { FileUploadService } from './fileUpload/FileUploadService'; +import { UploadAudioFromYoutubeRequest } from './fileUpload/fromYoutube/UploadAudioFromYoutubeRequest'; +import { uploadAudioFromYoutube } from './fileUpload/fromYoutube/UploadFromYoutubeService'; @Controller() export class AppController { @@ -12,7 +14,11 @@ export class AppController { @Post('/upload/audio') @UseInterceptors(FileInterceptor('file')) - uploadTrack(@UploadedFile() file: Express.Multer.File) { + uploadAudio(@UploadedFile() file: Express.Multer.File) { return this.fileUploadService.handleFileUpload(file); } + @Post('/upload/audio/from-youtube') + uploadAudioFromYoutube(@Body() uploadRequest: UploadAudioFromYoutubeRequest) { + return uploadAudioFromYoutube(uploadRequest); + } } diff --git a/apps/audio-file-uploader/src/app/fileUpload/fromYoutube/UploadAudioFromYoutubeRequest.ts b/apps/audio-file-uploader/src/app/fileUpload/fromYoutube/UploadAudioFromYoutubeRequest.ts new file mode 100644 index 0000000..7fe673d --- /dev/null +++ b/apps/audio-file-uploader/src/app/fileUpload/fromYoutube/UploadAudioFromYoutubeRequest.ts @@ -0,0 +1,3 @@ +export interface UploadAudioFromYoutubeRequest{ + urls: string[] +} \ No newline at end of file diff --git a/apps/audio-file-uploader/src/app/fileUpload/fromYoutube/UploadAudioFromYoutubeResponse.ts b/apps/audio-file-uploader/src/app/fileUpload/fromYoutube/UploadAudioFromYoutubeResponse.ts new file mode 100644 index 0000000..ff822e2 --- /dev/null +++ b/apps/audio-file-uploader/src/app/fileUpload/fromYoutube/UploadAudioFromYoutubeResponse.ts @@ -0,0 +1,10 @@ +export interface UploadAudioFromYoutubeResponse { + uploadResult: UploadAudioFromYoutubeResponseForUrl[]; + uploadedFilesLinks: string[] +} + +export interface UploadAudioFromYoutubeResponseForUrl { + url: string; + status: string; + uploadedFile?: string; +} diff --git a/apps/audio-file-uploader/src/app/fileUpload/fromYoutube/UploadFromYoutubeService.ts b/apps/audio-file-uploader/src/app/fileUpload/fromYoutube/UploadFromYoutubeService.ts new file mode 100644 index 0000000..0af541c --- /dev/null +++ b/apps/audio-file-uploader/src/app/fileUpload/fromYoutube/UploadFromYoutubeService.ts @@ -0,0 +1,68 @@ +import { UploadAudioFromYoutubeRequest } from './UploadAudioFromYoutubeRequest'; +import { UploadAudioFromYoutubeResponse, UploadAudioFromYoutubeResponseForUrl } from './UploadAudioFromYoutubeResponse'; +import { Logger } from '@nestjs/common'; +import ytdl from '@distube/ytdl-core'; +import ffmpeg from 'fluent-ffmpeg'; +import { join } from 'path'; +import { createWriteStream } from 'fs'; +import process from 'node:process'; + +const UPLOAD_DIRECTORY = `${process.env.FILESERVER_PATH ? process.env.FILESERVER_PATH : '.'}/uploads`; + +export async function uploadAudioFromYoutube( + uploadRequest: UploadAudioFromYoutubeRequest +): Promise { + let i = 0; + const uploadedFilesLinks = []; + + const uploadResult = await Promise.all( + uploadRequest.urls.map(async (url) => { + if (i > 0) { + await sleep(200); + } + i++; + Logger.log(`begin downloading ${url}`); + if (!ytdl.validateURL(url)) { + return { url: url, status: 'error', message: 'Invalid YouTube URL' }; + } + + try { + const info = await ytdl.getInfo(url); + const fileName = (info.videoDetails.title ?? `audio_${Date.now()}`).replace(/[^a-zA-Z0-9]/g, '_') + '.mp3'; + const filePath = join(UPLOAD_DIRECTORY, fileName); + const audioStream = ytdl(url, { filter: 'audioonly' }); + + await new Promise((resolve, reject) => { + const fileStream = createWriteStream(filePath); + ffmpeg(audioStream) + .audioCodec('libmp3lame') + .format('mp3') + .on('error', (err) => { + Logger.error(`Error converting to MP3 for URL ${url}: ${err.message}`); + console.trace(err); + fileStream.destroy(err); + reject(err); + }) + .on('end', () => { + Logger.log(`MP3 saved at: ${filePath}`); + resolve(null); + }) + .pipe(fileStream); + }); + uploadedFilesLinks.push(`https://fourgate.cloud/public/musics/uploads/${fileName}`); + return { url: url, status: 'ok', uploadedFile: fileName }; + } catch (error) { + Logger.error(`Error while downloading audio from URL ${url}: ${error.message}`); + console.trace(error); + return { url: url, status: 'error', message: error.message }; + } + }) + ); + return { uploadResult: uploadResult, uploadedFilesLinks: uploadedFilesLinks }; +} + +function sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} diff --git a/apps/audio-file-uploader/src/main.ts b/apps/audio-file-uploader/src/main.ts index 0c89075..cdfd9f8 100644 --- a/apps/audio-file-uploader/src/main.ts +++ b/apps/audio-file-uploader/src/main.ts @@ -9,7 +9,8 @@ async function bootstrap() { app.setGlobalPrefix(globalPrefix); app.enableCors({origin: '*', allowedHeaders: ["Origin", "X-Requested-With", "Content-Type", "Accept"]});// TODO fix this const port = process.env.PORT || 3000; - await app.listen(port); + const server = await app.listen(port); + server.setTimeout(3600000); Logger.log(`🚀 Application audio-file-uploader is running on: http://localhost:${port}/${globalPrefix}`); } diff --git a/package-lock.json b/package-lock.json index f714a6a..3255633 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "license": "MIT", "dependencies": { + "@distube/ytdl-core": "4.15.4", "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@mui/icons-material": "^6.1.6", @@ -2316,6 +2317,50 @@ "node": ">=10.0.0" } }, + "node_modules/@distube/ytdl-core": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/@distube/ytdl-core/-/ytdl-core-4.15.4.tgz", + "integrity": "sha512-KpTgBNYXk7j/M/fCQAa8qIXISgNTliaV6mVZHTZ/h/8vSsrEEgNgSEATPtPY99cAuEAJkoVETKEy1Qq/9iZVgw==", + "deprecated": "This version is deprecated, please upgrade to the latest version.", + "license": "MIT", + "dependencies": { + "http-cookie-agent": "^6.0.8", + "https-proxy-agent": "^7.0.6", + "m3u8stream": "^0.8.6", + "miniget": "^4.2.3", + "sax": "^1.4.1", + "tough-cookie": "^4.1.4", + "undici": "five" + }, + "engines": { + "node": ">=14.0" + }, + "funding": { + "url": "https://github.com/distubejs/ytdl-core?sponsor" + } + }, + "node_modules/@distube/ytdl-core/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@distube/ytdl-core/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@emnapi/core": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz", @@ -18052,6 +18097,39 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/http-cookie-agent": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/http-cookie-agent/-/http-cookie-agent-6.0.8.tgz", + "integrity": "sha512-qnYh3yLSr2jBsTYkw11elq+T361uKAJaZ2dR4cfYZChw1dt9uL5t3zSUwehoqqVb4oldk1BpkXKm2oat8zV+oA==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.3" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/3846masa" + }, + "peerDependencies": { + "tough-cookie": "^4.0.0 || ^5.0.0", + "undici": "^5.11.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "undici": { + "optional": true + } + } + }, + "node_modules/http-cookie-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", @@ -21055,6 +21133,19 @@ "lz-string": "bin/bin.js" } }, + "node_modules/m3u8stream": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.6.tgz", + "integrity": "sha512-LZj8kIVf9KCphiHmH7sbFQTVe4tOemb202fWwvJwR9W5ENW/1hxJN6ksAWGhQgSBSa3jyWhnjKU1Fw1GaOdbyA==", + "license": "MIT", + "dependencies": { + "miniget": "^4.2.2", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.8", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", @@ -21473,6 +21564,15 @@ "webpack": "^5.0.0" } }, + "node_modules/miniget": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/miniget/-/miniget-4.2.3.tgz", + "integrity": "sha512-SjbDPDICJ1zT+ZvQwK0hUcRY4wxlhhNpHL9nJOB2MEAXRGagTljsO8MEDzQMTFf0Q8g4QNi8P9lEm/g7e+qgzA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -26308,7 +26408,6 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true, "license": "MIT" }, "node_modules/pump": { @@ -26326,7 +26425,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -26380,7 +26478,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true, "license": "MIT" }, "node_modules/queue-microtask": { @@ -27080,7 +27177,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, "license": "MIT" }, "node_modules/reselect": { @@ -27483,9 +27579,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "dev": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/saxes": { "version": "6.0.0", @@ -29729,7 +29823,6 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "psl": "^1.1.33", @@ -29745,7 +29838,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -30239,12 +30331,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "license": "MIT" }, + "node_modules/undici/node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", @@ -30389,7 +30502,6 @@ "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, "license": "MIT", "dependencies": { "querystringify": "^2.1.1", diff --git a/package.json b/package.json index 79ca1a5..c3d0115 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": {}, "private": true, "dependencies": { + "@distube/ytdl-core": "4.15.4", "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@mui/icons-material": "^6.1.6",