Replace yt-search dependency with yt-dlp search

This commit is contained in:
Tor Wingalen
2026-05-15 23:36:57 +02:00
parent 005e47bf6e
commit f64cc97475
3 changed files with 97 additions and 300 deletions
-2
View File
@@ -31,7 +31,6 @@
"@nestjs/serve-static": "^4.0.2",
"@nestjs/typeorm": "^10.0.2",
"@nestjs/websockets": "^10.3.10",
"@types/yt-search": "^2.10.3",
"bullmq": "^5.31.2",
"isomorphic-unfetch": "^4.0.2",
"node-id3": "^0.2.9",
@@ -39,7 +38,6 @@
"rxjs": "7.8.0",
"spotify-url-info": "^3.2.18",
"sqlite3": "^5.1.7",
"yt-search": "^2.12.1",
"ytdlp-nodejs": "^3.4.4"
},
"devDependencies": {
+69 -7
View File
@@ -3,7 +3,6 @@ import { TrackEntity } from '../track/track.entity';
import { EnvironmentEnum } from '../environmentEnum';
import { TrackService } from '../track/track.service';
import { ConfigService } from '@nestjs/config';
import * as yts from 'yt-search';
import * as fs from 'fs';
import { spawn } from 'child_process';
const NodeID3 = require('node-id3');
@@ -63,6 +62,16 @@ interface CandidateScore extends YoutubeMatch {
rejected: boolean;
}
interface YtDlpSearchVideo {
url?: string;
webpage_url?: string;
id?: string;
title?: string;
uploader?: string;
channel?: string;
duration?: number;
}
@Injectable()
export class YoutubeService {
private readonly logger = new Logger(TrackService.name);
@@ -78,14 +87,14 @@ export class YoutubeService {
const query = `${artist} - ${name}`;
this.logger.debug(`Searching ${query} on YT`);
const result = await yts(query);
const videos = await this.searchYoutubeVideos(query);
const excludedVideoIds = new Set(
excludedUrls
.map((url) => this.getYoutubeVideoId(url))
.filter((id): id is string => !!id),
);
const candidates = (result.videos || [])
const candidates = videos
.filter((video: any) => !!video?.url && !!video?.title)
.filter((video: any) => {
const videoId = this.getYoutubeVideoId(String(video.url));
@@ -130,6 +139,55 @@ export class YoutubeService {
return accepted;
}
private async searchYoutubeVideos(query: string): Promise<YtDlpSearchVideo[]> {
const searchTarget = `ytsearch15:${query}`;
const args = [
'--dump-single-json',
'--flat-playlist',
'--skip-download',
'--no-playlist',
'--no-cache-dir',
'--no-cookies-from-browser',
'--add-header',
`User-Agent:${HEADERS['User-Agent']}`,
searchTarget,
];
const output = await this.runYtDlpForOutput(args);
let parsed: any;
try {
parsed = JSON.parse(output);
} catch (error) {
throw new Error(`Failed to parse yt-dlp search output: ${(error as Error).message}`);
}
const entries = Array.isArray(parsed?.entries) ? parsed.entries : [];
return entries
.filter((entry: YtDlpSearchVideo) => !!entry?.title && (!!entry?.url || !!entry?.webpage_url || !!entry?.id))
.map((entry: YtDlpSearchVideo) => ({
...entry,
url: this.normalizeYtDlpVideoUrl(entry),
author: entry.uploader || entry.channel || '',
seconds: typeof entry.duration === 'number' ? entry.duration : undefined,
}));
}
private normalizeYtDlpVideoUrl(video: YtDlpSearchVideo): string {
const rawUrl = String(video.webpage_url || video.url || '');
if (rawUrl.startsWith('http://') || rawUrl.startsWith('https://')) {
return rawUrl;
}
const id = String(video.id || rawUrl || '').trim();
if (!id) {
return '';
}
return `https://www.youtube.com/watch?v=${id}`;
}
private scoreCandidate(
video: any,
artist: string,
@@ -343,7 +401,7 @@ export class YoutubeService {
return tempCookiesFile;
}
private runYtDlp(args: string[]): Promise<void> {
private runYtDlpForOutput(args: string[]): Promise<string> {
return new Promise((resolvePromise, rejectPromise) => {
const child = spawn('yt-dlp', args, {
stdio: ['ignore', 'pipe', 'pipe'],
@@ -352,11 +410,11 @@ export class YoutubeService {
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => {
child.stdout?.on('data', (chunk) => {
stdout += chunk.toString();
});
child.stderr.on('data', (chunk) => {
child.stderr?.on('data', (chunk) => {
stderr += chunk.toString();
});
@@ -366,7 +424,7 @@ export class YoutubeService {
child.on('close', (code) => {
if (code === 0) {
resolvePromise();
resolvePromise(stdout);
return;
}
@@ -379,6 +437,10 @@ export class YoutubeService {
});
}
private async runYtDlp(args: string[]): Promise<void> {
await this.runYtDlpForOutput(args);
}
async downloadAndFormat(track: TrackEntity, output: string): Promise<void> {
this.logger.debug(
`Downloading ${track.artist} - ${track.name} (${track.youtubeUrl}) from YT`,