128 lines
4.4 KiB
TypeScript
128 lines
4.4 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import {videoDir, videoTempDir} from "../index";
|
|
import ffmpeg from "fluent-ffmpeg";
|
|
import Innertube, {Platform, Types} from "youtubei.js";
|
|
import {Readable} from "node:stream";
|
|
import {logError} from "./utils";
|
|
import {performFFmpeg} from "./ffmpeg";
|
|
|
|
export function getYouTubeVideoId(url: string): string {
|
|
const regex = /(?:(?:youtube\.com|music\.youtube\.com)\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?|shorts|clip)\/|.*[?&]v=)|youtu\.be\/)([^"&?/\s]{11})/i;
|
|
const match = url.match(regex);
|
|
if (!match || !match[1]) throw new Error("Invalid YouTube or Shorts URL");
|
|
return match[1];
|
|
}
|
|
|
|
export async function downloadVideoFromYouTube(url: string): Promise<{
|
|
time: number,
|
|
exists?: boolean,
|
|
buffer: Buffer | null
|
|
}> {
|
|
const start = Date.now();
|
|
let buffer: Buffer | null = null;
|
|
|
|
try {
|
|
const videoId = getYouTubeVideoId(url);
|
|
const filePath = path.join(videoDir, `${videoId}.mp4`);
|
|
if (fs.existsSync(filePath)) {
|
|
const buffer = Buffer.from(fs.readFileSync(filePath));
|
|
return {
|
|
time: Date.now() - start,
|
|
exists: true,
|
|
buffer: buffer
|
|
};
|
|
}
|
|
|
|
Platform.shim.eval = async (data: Types.BuildScriptResult, env: Record<string, Types.VMPrimative>) => {
|
|
const properties = [];
|
|
if (env.n) properties.push(`n: exportedVars.nFunction("${env.n}")`);
|
|
if (env.sig) properties.push(`sig: exportedVars.sigFunction("${env.sig}")`);
|
|
|
|
const code = `${data.output}\nreturn { ${properties.join(", ")} }`;
|
|
return new Function(code)();
|
|
};
|
|
const yt = await Innertube.create({
|
|
generate_session_locally: true,
|
|
retrieve_player: true
|
|
});
|
|
|
|
const videoInfo = await yt.getInfo(videoId, {client: "ANDROID"});
|
|
|
|
console.log(`Fetching metadata for: ${videoId}...`);
|
|
|
|
const targetQuality = "360p";
|
|
|
|
const videoFormat = videoInfo.streaming_data?.formats.find(f => f.quality_label.startsWith(targetQuality))
|
|
|| videoInfo.streaming_data?.adaptive_formats.find(f => f.quality_label.startsWith(targetQuality));
|
|
|
|
const audioFormat = videoInfo.chooseFormat({type: "audio", quality: "best", language: "original"});
|
|
|
|
console.log("Video format: ", videoFormat);
|
|
console.log("Audio Format: ", audioFormat);
|
|
|
|
if (!videoFormat) {
|
|
console.log(`Quality ${targetQuality} not found. Falling back to best available.`);
|
|
}
|
|
|
|
const videoWebStream = await videoInfo.download({
|
|
itag: videoFormat.itag,
|
|
client: "ANDROID"
|
|
});
|
|
|
|
const audioWebStream = await videoInfo.download({
|
|
itag: audioFormat.itag,
|
|
client: "ANDROID"
|
|
});
|
|
|
|
const videoStream = Readable.fromWeb(videoWebStream as any);
|
|
const audioStream = Readable.fromWeb(audioWebStream as any);
|
|
|
|
const videoPath = path.join(videoTempDir, `temp_video_${videoId}.mp4`);
|
|
const audioPath = path.join(videoTempDir, `temp_audio_${videoId}.mp4`);
|
|
|
|
const writeStream = (stream: any, path: string) =>
|
|
new Promise((resolve, reject) => {
|
|
const file = fs.createWriteStream(path);
|
|
stream.pipe(file);
|
|
file.on("finish", resolve);
|
|
file.on("error", reject);
|
|
});
|
|
|
|
await Promise.all([
|
|
writeStream(videoStream, videoPath),
|
|
writeStream(audioStream, audioPath)
|
|
]);
|
|
|
|
await performFFmpeg(() =>
|
|
ffmpeg()
|
|
.input(videoPath)
|
|
.input(audioPath)
|
|
.videoCodec("copy")
|
|
.audioCodec("copy")
|
|
.save(filePath)
|
|
.on("progress", (progress) => {
|
|
console.log("progress", progress);
|
|
})
|
|
).catch(logError);
|
|
|
|
fs.unlinkSync(videoPath);
|
|
fs.unlinkSync(audioPath);
|
|
|
|
buffer = fs.readFileSync(filePath);
|
|
|
|
console.log(`✅ Saved to ${videoId}.mp4`);
|
|
} catch (error) {
|
|
console.error("❌ Download failed:", error instanceof Error ? error.message : error);
|
|
throw error;
|
|
}
|
|
|
|
const end = Date.now();
|
|
const diff = end - start;
|
|
console.log(`Video downloaded. URL: ${url}\ntook ${diff}ms`);
|
|
|
|
return {
|
|
time: diff,
|
|
buffer: buffer,
|
|
};
|
|
} |