fix(downloading): migrate from @distube/ytdl-core to ytdlp-nodejs yt downloading library
This commit is contained in:
@@ -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 .
|
||||||
|
|||||||
Generated
+16
-4
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user