From 5795f7cc178ab4d8a8d3d8a7e94f33743ff6e9e0 Mon Sep 17 00:00:00 2001 From: Raiper34 Date: Sat, 15 Nov 2025 14:50:17 +0000 Subject: [PATCH] fix(downloading): migrate from @distube/ytdl-core to ytdlp-nodejs yt downloading library --- .github/workflows/update-ytdl.yml | 4 +- package-lock.json | 20 ++++- package.json | 2 +- src/backend/package.json | 4 +- src/backend/src/shared/youtube.service.ts | 100 ++++++---------------- 5 files changed, 48 insertions(+), 82 deletions(-) diff --git a/.github/workflows/update-ytdl.yml b/.github/workflows/update-ytdl.yml index a70a5d4..8e88965 100644 --- a/.github/workflows/update-ytdl.yml +++ b/.github/workflows/update-ytdl.yml @@ -22,7 +22,7 @@ jobs: node-version: '18.x' - name: Set up docker builder run: docker buildx create --name container --driver=docker-container - - name: Update @distube/ytdl-core and release + - name: Update ytdlp-nodejs and release run: | docker login --username=${{ secrets.DOCKER_USERNAME }} --password=${{ secrets.DOCKER_PASSWORD }} npm ci --only=dev @@ -30,7 +30,7 @@ jobs: if git diff --exit-code; then echo "NO changes detected" else - npm update @distube/ytdl-core -w backend + npm update ytdlp-nodejs -w backend git config --global user.email "updater@spooty" git config --global user.name "AutoUpdater" git add . diff --git a/package-lock.json b/package-lock.json index 18233fa..0000228 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20361,15 +20361,26 @@ "yt-search-video": "bin/mpv_video.sh" } }, + "node_modules/ytdlp-nodejs": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/ytdlp-nodejs/-/ytdlp-nodejs-2.3.5.tgz", + "integrity": "sha512-7V08DRv8C1K0HxJFvRoaoLYFS/reJ9VJBlaMVhEvdi2IsYK/9Hae1Mah65Y+bhk3RAmx7G9eTfpOhkj3bp0Zbw==", + "hasInstallScript": true, + "bin": { + "ytdlp-nodejs": "dist/cli.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/zone.js": { "version": "0.15.0", "license": "MIT" }, "src/backend": { - "version": "2.1.1", + "version": "2.2.0", "license": "UNLICENSED", "dependencies": { - "@distube/ytdl-core": "^4.16.12", "@nestjs/bullmq": "^10.2.3", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.2", @@ -20390,7 +20401,8 @@ "rxjs": "^7.8.1", "spotify-url-info": "^3.2.18", "sqlite3": "^5.1.7", - "yt-search": "^2.12.1" + "yt-search": "^2.12.1", + "ytdlp-nodejs": "^2.3.5" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -20825,7 +20837,7 @@ } }, "src/frontend": { - "version": "2.1.1", + "version": "2.2.0", "dependencies": { "@angular/animations": "^19.0.6", "@angular/common": "^19.0.6", diff --git a/package.json b/package.json index 88fb572..5e4166e 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "changelog": "auto-changelog -p", "release": "release-it", "commit": "cz", - "check:lib": "npm-check-updates -w backend -f @distube/ytdl-core -u" + "check:lib": "npm-check-updates -w backend -f ytdlp-nodejs -u" }, "devDependencies": { "@release-it/bumper": "^7.0.1", diff --git a/src/backend/package.json b/src/backend/package.json index 4ef4ae1..a9396d1 100644 --- a/src/backend/package.json +++ b/src/backend/package.json @@ -21,7 +21,6 @@ "gen": "nest generate" }, "dependencies": { - "@distube/ytdl-core": "^4.16.12", "@nestjs/bullmq": "^10.2.3", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.2", @@ -42,7 +41,8 @@ "rxjs": "^7.8.1", "spotify-url-info": "^3.2.18", "sqlite3": "^5.1.7", - "yt-search": "^2.12.1" + "yt-search": "^2.12.1", + "ytdlp-nodejs": "^2.3.5" }, "devDependencies": { "@nestjs/cli": "^10.0.0", diff --git a/src/backend/src/shared/youtube.service.ts b/src/backend/src/shared/youtube.service.ts index 66bfbb2..d446dee 100644 --- a/src/backend/src/shared/youtube.service.ts +++ b/src/backend/src/shared/youtube.service.ts @@ -3,46 +3,20 @@ import { TrackEntity } from '../track/track.entity'; import { EnvironmentEnum } from '../environmentEnum'; import { TrackService } from '../track/track.service'; import { ConfigService } from '@nestjs/config'; -import * as fs from 'fs'; -import * as ffmpeg from 'fluent-ffmpeg'; +import { YtDlp } from 'ytdlp-nodejs'; import * as yts from 'yt-search'; -import * as ytdl from '@distube/ytdl-core'; -import { Readable } from 'stream'; const NodeID3 = require('node-id3'); -enum StreamStates { - Finish = 'finish', - Error = 'error', -} - -function parseCookies( - cookieString?: string, -): { name: string; value: string }[] | undefined { - if (!cookieString) return undefined; - return cookieString - .split(';') - .map((c) => { - const [name, ...rest] = c.split('='); - return { name: name.trim(), value: rest.join('=').trim() }; - }) - .filter((c) => c.name && c.value); -} +const HEADERS = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', +}; @Injectable() export class YoutubeService { private readonly logger = new Logger(TrackService.name); - private readonly ytCookies: string | undefined; - private readonly ytAgent: any | undefined; - constructor(private readonly configService: ConfigService) { - this.ytCookies = this.configService.get('YT_COOKIES'); - if (this.ytCookies) { - const cookiesArr = parseCookies(this.ytCookies); - if (cookiesArr && cookiesArr.length > 0) { - this.ytAgent = ytdl.createAgent(cookiesArr); - } - } - } + constructor(private readonly configService: ConfigService) {} async findOnYoutubeOne(artist: string, name: string): Promise { this.logger.debug(`Searching ${artist} - ${name} on YT`); @@ -51,26 +25,31 @@ export class YoutubeService { return url; } - downloadAndFormat(track: TrackEntity, folderName: string): Promise { + async downloadAndFormat( + track: TrackEntity, + output: string, + ): Promise { this.logger.debug( `Downloading ${track.artist} - ${track.name} (${track.youtubeUrl}) from YT`, ); - return new Promise((res, reject) => { - ffmpeg(this.getYoutubeAudio(track.youtubeUrl, reject)) - .format(this.configService.get(EnvironmentEnum.FORMAT)) - .on(StreamStates.Error, (err) => reject(err)) - .pipe( - fs - .createWriteStream(folderName) - .on(StreamStates.Finish, () => { - this.logger.debug( - `Downloaded ${track.artist} - ${track.name} to ${folderName}`, - ); - res(); - }) - .on(StreamStates.Error, (err) => reject(err)), - ); + if (!track.youtubeUrl) { + this.logger.error('youtubeUrl is null or undefined'); + throw Error('youtubeUrl is null or undefined'); + } + const ytdlp = new YtDlp(); + await ytdlp.downloadAsync(track.youtubeUrl, { + format: { + filter: 'audioonly', + type: this.configService.get<'m4a'>(EnvironmentEnum.FORMAT), + quality: 0, + }, + output, + cookiesFromBrowser: this.configService.get('YT_COOKIES'), + headers: HEADERS, }); + this.logger.debug( + `Downloaded ${track.artist} - ${track.name} to ${output}`, + ); } async addImage( @@ -99,29 +78,4 @@ export class YoutubeService { ); } } - - private getYoutubeAudio( - youtubeUrl: string, - reject: (reason: any) => void, - ): Readable { - if (!youtubeUrl) { - this.logger.error('youtubeUrl is null or undefined'); - reject('youtubeUrl is null or undefined'); - return null; - } - const options: ytdl.downloadOptions = { - quality: 'highestaudio', - filter: 'audioonly', - agent: this.ytAgent, - requestOptions: { - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - }, - }, - }; - return ytdl(youtubeUrl, options).on(StreamStates.Error, (err) => - reject(err), - ); - } }