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'
- 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 .
+16 -4
View File
@@ -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",
+1 -1
View File
@@ -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",
+2 -2
View File
@@ -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",
+27 -73
View File
@@ -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<string>('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<string> {
this.logger.debug(`Searching ${artist} - ${name} on YT`);
@@ -51,26 +25,31 @@ export class YoutubeService {
return url;
}
downloadAndFormat(track: TrackEntity, folderName: string): Promise<void> {
async downloadAndFormat(
track: TrackEntity,
output: string,
): Promise<void> {
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<string>(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<string>('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),
);
}
}