fix(downloading): migrate from @distube/ytdl-core to ytdlp-nodejs yt downloading library

This commit is contained in:
Raiper34
2025-11-15 15:05:31 +00:00
parent 4be3743f7b
commit 5795f7cc17
5 changed files with 48 additions and 82 deletions
+2 -2
View File
@@ -22,7 +22,7 @@ jobs:
node-version: '18.x' node-version: '18.x'
- name: Set up docker builder - name: Set up docker builder
run: docker buildx create --name container --driver=docker-container run: docker buildx create --name container --driver=docker-container
- name: Update @distube/ytdl-core and release - name: Update ytdlp-nodejs and release
run: | run: |
docker login --username=${{ secrets.DOCKER_USERNAME }} --password=${{ secrets.DOCKER_PASSWORD }} docker login --username=${{ secrets.DOCKER_USERNAME }} --password=${{ secrets.DOCKER_PASSWORD }}
npm ci --only=dev npm ci --only=dev
@@ -30,7 +30,7 @@ jobs:
if git diff --exit-code; then if git diff --exit-code; then
echo "NO changes detected" echo "NO changes detected"
else 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.email "updater@spooty"
git config --global user.name "AutoUpdater" git config --global user.name "AutoUpdater"
git add . git add .
+16 -4
View File
@@ -20361,15 +20361,26 @@
"yt-search-video": "bin/mpv_video.sh" "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": { "node_modules/zone.js": {
"version": "0.15.0", "version": "0.15.0",
"license": "MIT" "license": "MIT"
}, },
"src/backend": { "src/backend": {
"version": "2.1.1", "version": "2.2.0",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@distube/ytdl-core": "^4.16.12",
"@nestjs/bullmq": "^10.2.3", "@nestjs/bullmq": "^10.2.3",
"@nestjs/common": "^10.0.0", "@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.2", "@nestjs/config": "^3.2.2",
@@ -20390,7 +20401,8 @@
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"spotify-url-info": "^3.2.18", "spotify-url-info": "^3.2.18",
"sqlite3": "^5.1.7", "sqlite3": "^5.1.7",
"yt-search": "^2.12.1" "yt-search": "^2.12.1",
"ytdlp-nodejs": "^2.3.5"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^10.0.0", "@nestjs/cli": "^10.0.0",
@@ -20825,7 +20837,7 @@
} }
}, },
"src/frontend": { "src/frontend": {
"version": "2.1.1", "version": "2.2.0",
"dependencies": { "dependencies": {
"@angular/animations": "^19.0.6", "@angular/animations": "^19.0.6",
"@angular/common": "^19.0.6", "@angular/common": "^19.0.6",
+1 -1
View File
@@ -18,7 +18,7 @@
"changelog": "auto-changelog -p", "changelog": "auto-changelog -p",
"release": "release-it", "release": "release-it",
"commit": "cz", "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": { "devDependencies": {
"@release-it/bumper": "^7.0.1", "@release-it/bumper": "^7.0.1",
+2 -2
View File
@@ -21,7 +21,6 @@
"gen": "nest generate" "gen": "nest generate"
}, },
"dependencies": { "dependencies": {
"@distube/ytdl-core": "^4.16.12",
"@nestjs/bullmq": "^10.2.3", "@nestjs/bullmq": "^10.2.3",
"@nestjs/common": "^10.0.0", "@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.2", "@nestjs/config": "^3.2.2",
@@ -42,7 +41,8 @@
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"spotify-url-info": "^3.2.18", "spotify-url-info": "^3.2.18",
"sqlite3": "^5.1.7", "sqlite3": "^5.1.7",
"yt-search": "^2.12.1" "yt-search": "^2.12.1",
"ytdlp-nodejs": "^2.3.5"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^10.0.0", "@nestjs/cli": "^10.0.0",
+27 -73
View File
@@ -3,46 +3,20 @@ import { TrackEntity } from '../track/track.entity';
import { EnvironmentEnum } from '../environmentEnum'; import { EnvironmentEnum } from '../environmentEnum';
import { TrackService } from '../track/track.service'; import { TrackService } from '../track/track.service';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import * as fs from 'fs'; import { YtDlp } from 'ytdlp-nodejs';
import * as ffmpeg from 'fluent-ffmpeg';
import * as yts from 'yt-search'; import * as yts from 'yt-search';
import * as ytdl from '@distube/ytdl-core';
import { Readable } from 'stream';
const NodeID3 = require('node-id3'); const NodeID3 = require('node-id3');
enum StreamStates { const HEADERS = {
Finish = 'finish', 'User-Agent':
Error = 'error', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
} };
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);
}
@Injectable() @Injectable()
export class YoutubeService { export class YoutubeService {
private readonly logger = new Logger(TrackService.name); private readonly logger = new Logger(TrackService.name);
private readonly ytCookies: string | undefined;
private readonly ytAgent: any | undefined;
constructor(private readonly configService: ConfigService) { constructor(private readonly configService: ConfigService) {}
this.ytCookies = this.configService.get<string>('YT_COOKIES');
if (this.ytCookies) {
const cookiesArr = parseCookies(this.ytCookies);
if (cookiesArr && cookiesArr.length > 0) {
this.ytAgent = ytdl.createAgent(cookiesArr);
}
}
}
async findOnYoutubeOne(artist: string, name: string): Promise<string> { async findOnYoutubeOne(artist: string, name: string): Promise<string> {
this.logger.debug(`Searching ${artist} - ${name} on YT`); this.logger.debug(`Searching ${artist} - ${name} on YT`);
@@ -51,26 +25,31 @@ export class YoutubeService {
return url; return url;
} }
downloadAndFormat(track: TrackEntity, folderName: string): Promise<void> { async downloadAndFormat(
track: TrackEntity,
output: string,
): Promise<void> {
this.logger.debug( this.logger.debug(
`Downloading ${track.artist} - ${track.name} (${track.youtubeUrl}) from YT`, `Downloading ${track.artist} - ${track.name} (${track.youtubeUrl}) from YT`,
); );
return new Promise((res, reject) => { if (!track.youtubeUrl) {
ffmpeg(this.getYoutubeAudio(track.youtubeUrl, reject)) this.logger.error('youtubeUrl is null or undefined');
.format(this.configService.get<string>(EnvironmentEnum.FORMAT)) throw Error('youtubeUrl is null or undefined');
.on(StreamStates.Error, (err) => reject(err)) }
.pipe( const ytdlp = new YtDlp();
fs await ytdlp.downloadAsync(track.youtubeUrl, {
.createWriteStream(folderName) format: {
.on(StreamStates.Finish, () => { filter: 'audioonly',
this.logger.debug( type: this.configService.get<'m4a'>(EnvironmentEnum.FORMAT),
`Downloaded ${track.artist} - ${track.name} to ${folderName}`, quality: 0,
); },
res(); output,
}) cookiesFromBrowser: this.configService.get<string>('YT_COOKIES'),
.on(StreamStates.Error, (err) => reject(err)), headers: HEADERS,
);
}); });
this.logger.debug(
`Downloaded ${track.artist} - ${track.name} to ${output}`,
);
} }
async addImage( 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),
);
}
} }