Skip to content

Commit 1343dea

Browse files
committed
Refactor YouTube video handling to use our new yt-dlp implementation #94 #126 #127
1 parent 432eeec commit 1343dea

1 file changed

Lines changed: 128 additions & 57 deletions

File tree

src/index.ts

Lines changed: 128 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,24 @@ import { Streamer, Utils, prepareStream, playStream } from "@dank074/discord-vid
33
import config from "./config.js";
44
import fs from 'fs';
55
import path from 'path';
6-
import ytdl from '@distube/ytdl-core';
76
import { getStream, getVod } from 'twitch-m3u8';
87
import yts from 'play-dl';
98
import { getVideoParams, ffmpegScreenshot } from "./utils/ffmpeg.js";
109
import logger from './utils/logger.js';
10+
import { downloadExecutable, downloadToTempFile, checkForUpdatesAndUpdate } from './utils/yt-dlp.js';
1111
import { Youtube } from './utils/youtube.js';
1212
import { TwitchStream } from './@types/index.js';
1313

14+
// Download yt-dlp and check for updates
15+
(async () => {
16+
try {
17+
await downloadExecutable();
18+
await checkForUpdatesAndUpdate();
19+
} catch (error) {
20+
logger.error("Error during initial yt-dlp setup/update:", error);
21+
}
22+
})();
23+
1424
// Create a new instance of Streamer
1525
const streamer = new Streamer(new Client());
1626

@@ -172,7 +182,7 @@ streamer.client.on('messageCreate', async (message) => {
172182
sendPlaying(message, videoname || "Local Video");
173183

174184
// Play video
175-
playVideo(video.path, videoname);
185+
playVideo(message, video.path, videoname);
176186
}
177187
break;
178188
case 'playlink':
@@ -190,19 +200,20 @@ streamer.client.on('messageCreate', async (message) => {
190200
}
191201

192202
switch (true) {
193-
case ytdl.validateURL(link):
203+
case (link.includes('youtube.com/') || link.includes('youtu.be/')):
194204
{
195-
const [videoInfo, yturl] = await Promise.all([
196-
ytdl.getInfo(link),
197-
getVideoUrl(link).catch(error => {
198-
logger.error("Error:", error);
199-
return null;
200-
})
201-
]);
202-
203-
if (yturl) {
204-
sendPlaying(message, videoInfo.videoDetails.title);
205-
playVideo(yturl, videoInfo.videoDetails.title);
205+
try {
206+
const videoDetails = await youtube.getVideoInfo(link);
207+
208+
if (videoDetails && videoDetails.title) {
209+
playVideo(message, link, videoDetails.title);
210+
} else {
211+
logger.error(`Failed to get YouTube video info for link: ${link}.`);
212+
await sendError(message, 'Failed to process YouTube link.');
213+
}
214+
} catch (error) {
215+
logger.error(`Error processing YouTube link: ${link}`, error);
216+
await sendError(message, 'Error processing YouTube link.');
206217
}
207218
}
208219
break;
@@ -212,14 +223,14 @@ streamer.client.on('messageCreate', async (message) => {
212223
const twitchUrl = await getTwitchStreamUrl(link);
213224
if (twitchUrl) {
214225
sendPlaying(message, `${twitchId}'s Twitch Stream`);
215-
playVideo(twitchUrl, `twitch.tv/${twitchId}`);
226+
playVideo(message, twitchUrl, `twitch.tv/${twitchId}`);
216227
}
217228
}
218229
break;
219230
default:
220231
{
221232
sendPlaying(message, "URL");
222-
playVideo(link, "URL");
233+
playVideo(message, link, "URL");
223234
}
224235
}
225236
}
@@ -234,16 +245,15 @@ streamer.client.on('messageCreate', async (message) => {
234245
}
235246

236247
try {
237-
const [ytUrlFromTitle, searchResults] = await Promise.all([
238-
ytPlayTitle(title),
239-
yts.search(title, { limit: 1 })
240-
]);
241-
248+
const searchResults = await yts.search(title, { limit: 1 });
242249
const videoResult = searchResults[0];
243-
if (ytUrlFromTitle && videoResult?.title) {
244-
sendPlaying(message, videoResult.title);
245-
playVideo(ytUrlFromTitle, videoResult.title);
250+
251+
const searchResult = await youtube.searchAndGetPageUrl(title);
252+
253+
if (searchResult.pageUrl && searchResult.title) {
254+
playVideo(message, searchResult.pageUrl, searchResult.title);
246255
} else {
256+
logger.warn(`No video found or title missing for search: "${title}" using youtube.searchAndGetPageUrl.`);
247257
throw new Error('Could not find video');
248258
}
249259
} catch (error) {
@@ -419,54 +429,125 @@ streamer.client.on('messageCreate', async (message) => {
419429
});
420430

421431
// Function to play video
422-
async function playVideo(video: string, title?: string) {
423-
logger.info("Started playing " + video);
432+
async function playVideo(message: Message, videoSource: string, title?: string) {
433+
logger.info(`Attempting to play: ${title || videoSource}`);
424434
const [guildId, channelId, cmdChannelId] = [config.guildId, config.videoChannelId, config.cmdChannelId!];
425435

426-
// Reset manual stop flag
427436
streamStatus.manualStop = false;
428437

429-
// Join voice channel
430-
await streamer.joinVoice(guildId, channelId)
431-
streamStatus.joined = true;
432-
streamStatus.playing = true;
433-
streamStatus.channelInfo = {
434-
guildId: guildId,
435-
channelId: channelId,
436-
cmdChannelId: cmdChannelId
437-
}
438+
let inputForFfmpeg: any = videoSource;
439+
let tempFilePath: string | null = null;
440+
let downloadInProgressMessage: Message | null = null;
441+
let isLiveYouTubeStream = false;
438442

439443
try {
444+
if (typeof videoSource === 'string' && (videoSource.includes('youtube.com/') || videoSource.includes('youtu.be/'))) {
445+
const videoDetails = await youtube.getVideoInfo(videoSource);
446+
447+
if (videoDetails?.videoDetails?.isLiveContent) {
448+
isLiveYouTubeStream = true;
449+
logger.info(`YouTube video is live: ${title || videoSource}.`);
450+
const liveStreamUrl = await youtube.getLiveStreamUrl(videoSource);
451+
if (liveStreamUrl) {
452+
inputForFfmpeg = liveStreamUrl;
453+
logger.info(`Using direct live stream URL for ffmpeg: ${liveStreamUrl}`);
454+
} else {
455+
logger.error(`Failed to get live stream URL for ${title || videoSource}. Falling back to download attempt or error.`);
456+
await sendError(message, `Failed to get live stream URL for \`${title || 'YouTube live video'}\`.`);
457+
await cleanupStreamStatus();
458+
return;
459+
}
460+
} else {
461+
downloadInProgressMessage = await message.reply(`📥 Downloading \`${title || 'YouTube video'}\`...`).catch(e => {
462+
logger.warn("Failed to send 'Downloading...' message:", e);
463+
return null;
464+
});
465+
logger.info(`Downloading YouTube link with yt-dlp to temp file: ${videoSource}`);
466+
467+
const ytDlpDownloadOptions: Parameters<typeof downloadToTempFile>[1] = {
468+
format: `bestvideo[height<=${streamOpts.height || 720}][ext=mp4]+bestaudio[ext=m4a]/bestvideo[height<=${streamOpts.height || 720}]+bestaudio/best[height<=${streamOpts.height || 720}]/best`,
469+
noPlaylist: true,
470+
};
471+
472+
try {
473+
tempFilePath = await downloadToTempFile(videoSource, ytDlpDownloadOptions);
474+
inputForFfmpeg = tempFilePath;
475+
logger.info(`Using temp file for ffmpeg: ${tempFilePath}`);
476+
if (downloadInProgressMessage) {
477+
await downloadInProgressMessage.delete().catch(e => logger.warn("Failed to delete 'Downloading...' message:", e));
478+
}
479+
} catch (downloadError) {
480+
logger.error("Failed to download YouTube video:", downloadError);
481+
if (downloadInProgressMessage) {
482+
await downloadInProgressMessage.edit(`❌ Failed to download \`${title || 'YouTube video'}\`.`).catch(e => logger.warn("Failed to edit 'Downloading...' message:", e));
483+
} else {
484+
await sendError(message, `Failed to download video: ${downloadError instanceof Error ? downloadError.message : String(downloadError)}`);
485+
}
486+
await cleanupStreamStatus();
487+
return;
488+
}
489+
}
490+
}
491+
492+
await streamer.joinVoice(guildId, channelId);
493+
streamStatus.joined = true;
494+
streamStatus.playing = true;
495+
streamStatus.channelInfo = { guildId, channelId, cmdChannelId };
496+
440497
if (title) {
441498
streamer.client.user?.setActivity(status_watch(title) as ActivityOptions);
442499
}
500+
await sendPlaying(message, title || videoSource);
443501

444-
// Abort any existing controller
445502
controller?.abort();
446503
controller = new AbortController();
447504

448-
const { command, output } = prepareStream(video, streamOpts, controller.signal);
505+
const { command, output: ffmpegOutput } = prepareStream(inputForFfmpeg, streamOpts, controller.signal);
449506

450-
command.on("error", (err) => {
451-
logger.error("An error happened with ffmpeg", err);
507+
command.on("error", (err, stdout, stderr) => {
508+
logger.error("An error happened with ffmpeg:", err.message);
509+
if (stdout) logger.error("ffmpeg stdout:", stdout);
510+
if (stderr) logger.error("ffmpeg stderr:", stderr);
511+
if (!controller.signal.aborted) controller.abort();
512+
});
513+
514+
command.on("end", (stdout, stderr) => {
515+
logger.info(`ffmpeg processing finished successfully for ${title || videoSource}.`);
452516
});
453517

454-
await playStream(output, streamer, undefined, controller.signal)
455-
.catch(() => controller.abort());
518+
await playStream(ffmpegOutput, streamer, undefined, controller.signal)
519+
.catch((err) => {
520+
if (!controller.signal.aborted) {
521+
logger.error('playStream error:', err);
522+
}
523+
if (!controller.signal.aborted) controller.abort();
524+
});
525+
526+
if (!controller.signal.aborted) {
527+
logger.info(`Finished playing: ${title || videoSource}`);
528+
}
456529

457-
logger.info(`Finished playing video: ${video}`);
458530
} catch (error) {
459-
logger.error("Error occurred while playing video:", error);
460-
controller?.abort();
531+
logger.error(`Error in playVideo for ${title || videoSource}:`, error);
532+
if (!controller.signal.aborted) controller?.abort();
461533
} finally {
462534
await cleanupStreamStatus();
463-
if (!streamStatus.manualStop) {
535+
if (tempFilePath && !isLiveYouTubeStream) {
536+
try {
537+
logger.info(`Attempting to delete temp file: ${tempFilePath}`);
538+
fs.unlinkSync(tempFilePath);
539+
logger.info(`Successfully deleted temp file: ${tempFilePath}`);
540+
} catch (cleanupError) {
541+
logger.error(`Failed to delete temp file ${tempFilePath}:`, cleanupError);
542+
}
543+
}
544+
if (!streamStatus.manualStop && !controller.signal.aborted) {
464545
await sendFinishMessage();
465546
}
466547
}
467548
}
468549

469-
// Function to cleanup stream status - updated
550+
// Function to cleanup stream status
470551
async function cleanupStreamStatus() {
471552
if (streamStatus.manualStop) {
472553
return;
@@ -523,16 +604,6 @@ async function getTwitchStreamUrl(url: string): Promise<string | null> {
523604
}
524605
}
525606

526-
// Function to get video URL from YouTube
527-
async function getVideoUrl(videoUrl: string): Promise<string | null> {
528-
return await youtube.getVideoUrl(videoUrl);
529-
}
530-
531-
// Function to play video from YouTube
532-
async function ytPlayTitle(title: string): Promise<string | null> {
533-
return await youtube.searchAndPlay(title);
534-
}
535-
536607
// Function to search for videos on YouTube
537608
async function ytSearch(title: string): Promise<string[]> {
538609
return await youtube.search(title);

0 commit comments

Comments
 (0)