Skip to content

Commit b69a2b4

Browse files
authored
v3.1.0-beta.1
2 parents 095af66 + 9c900f5 commit b69a2b4

7 files changed

Lines changed: 569 additions & 237 deletions

File tree

app/api/video-converter/route.ts

Lines changed: 55 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,22 @@
22

33
import ffmpeg from "fluent-ffmpeg";
44
import ytdl from "@distube/ytdl-core";
5-
import { video_info } from "play-dl";
65
import { PassThrough, Readable } from "stream";
76
import path from "path";
87
import * as fs from "fs";
9-
import {
10-
detectFfmpegCapabilities,
11-
locateFfmpegPath,
12-
sanitizeFilename,
13-
} from "@/lib/serverUtils";
8+
import { initializeConf, sanitizeFilename } from "@/lib/serverUtils";
9+
import { FormatData, VideoData } from "@/lib/types";
10+
import { NextResponse } from "next/server";
1411

1512
const DIFFERENCE_TOLERANCE = 0.2;
1613
const PARENT_PATH =
1714
process.env.NODE_ENV === "production" ? "/temp/stroygetter" : "./temp";
1815
const CLEANUP_INTERVAL =
1916
process.env.NODE_ENV === "production" ? 1000 * 60 * 30 : 1000 * 60 * 2;
20-
21-
const buildReadableStream = (stream: PassThrough): ReadableStream => {
22-
return new ReadableStream({
23-
start(controller) {
24-
stream.on("data", (chunk: Buffer) => {
25-
controller.enqueue(chunk);
26-
});
27-
stream.on("end", () => {
28-
controller.close();
29-
});
30-
},
31-
});
17+
let CONF = {
18+
isInitialized: false,
19+
ffmpegPath: "",
20+
hasNvidiaCapabilities: false,
3221
};
3322

3423
const createTempDir = (tmp_dir: string) => {
@@ -155,31 +144,60 @@ export async function GET(request: Request) {
155144
return new Response("Missing quality parameter", { status: 400 });
156145
}
157146

158-
const ffmpegPath = await locateFfmpegPath();
147+
if (CONF.isInitialized === false) {
148+
CONF = await initializeConf(CONF);
149+
}
150+
151+
const ffmpegPath = CONF.ffmpegPath;
159152
if (!ffmpegPath) {
160153
console.error("FFmpeg path not found");
161154
return new Response("An error occurred in the server", { status: 500 });
162155
} else {
163156
ffmpeg.setFfmpegPath(ffmpegPath);
164157
}
165158

166-
const videoData = await video_info(url);
159+
const video = await ytdl.getBasicInfo(url);
160+
const formatMap = new Map();
161+
(video.player_response.streamingData.adaptiveFormats as FormatData[]).forEach(
162+
(format: FormatData) => {
163+
if (!formatMap.has(format.qualityLabel)) {
164+
formatMap.set(format.qualityLabel, format);
165+
}
166+
}
167+
);
167168

168-
if (!videoData || !videoData.video_details || !videoData.format) {
169+
const videoData: VideoData = {
170+
video_details: {
171+
title: video.videoDetails.title,
172+
description: video.videoDetails.description || "",
173+
duration: video.videoDetails.lengthSeconds,
174+
thumbnail: video.videoDetails.thumbnails[0].url,
175+
author: video.videoDetails.author.name,
176+
},
177+
format: Array.from(formatMap.values()),
178+
};
179+
180+
//const videoData = await ytdl.getBasicInfo(url);
181+
182+
if (!videoData) {
169183
return new Response("An error occurred while fetching video data", {
170184
status: 500,
171185
});
172186
}
173187

174-
const date = videoData.video_details.uploadedAt || new Date().toISOString();
188+
const date =
189+
video.player_response.microformat.playerMicroformatRenderer.publishDate ||
190+
new Date().toISOString();
175191

176192
const metadata = {
177-
title: videoData.video_details.title,
178-
artist: videoData.video_details.channel?.name || "Unknown artist",
179-
author: videoData.video_details.channel?.name || "Unknown author",
193+
title: videoData.video_details.title || "Unknown title",
194+
artist: videoData.video_details.author || "Unknown artist",
195+
author: videoData.video_details.author || "Unknown author",
180196
year: date.split("T")[0],
181-
genre: videoData.video_details.type || "Unknown genre",
182-
album: videoData.video_details.title,
197+
genre: video.videoDetails.keywords
198+
? video.videoDetails.keywords.join(", ")
199+
: "Unknown genre",
200+
album: videoData.video_details.title || "Unknown album",
183201
};
184202

185203
if (quality === "audio") {
@@ -211,7 +229,8 @@ export async function GET(request: Request) {
211229
})
212230
.pipe(audioPassThrough, { end: true });
213231

214-
return new Response(buildReadableStream(audioPassThrough), {
232+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
233+
return new NextResponse(audioPassThrough as any, {
215234
headers: {
216235
"Content-Type": "audio/mpeg",
217236
"Content-Disposition": `attachment; filename="${encodeURIComponent(
@@ -235,7 +254,7 @@ export async function GET(request: Request) {
235254
TEMP_DIR,
236255
`merged_${SANITIZED_TITLE}_${quality}_${Date.now()}.mp4`
237256
);
238-
const HAS_NVIDIA_GPU = await detectFfmpegCapabilities();
257+
const HAS_NVIDIA_GPU = CONF.hasNvidiaCapabilities;
239258

240259
try {
241260
createTempDir(TEMP_DIR);
@@ -260,11 +279,14 @@ export async function GET(request: Request) {
260279
);
261280

262281
const fileStream = fs.createReadStream(MERGED_FILE_PATH);
263-
//@ts-expect-error - L'argument de type readstream n'est pas attribuable au paramètre de type Passthrought.
264-
return new Response(buildReadableStream(fileStream), {
282+
283+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
284+
return new NextResponse(fileStream as any, {
265285
headers: {
266-
"Content-Type": "video/mp4",
267-
"Content-Disposition": `attachment; filename="output.mp4"`,
286+
"Content-Type": quality === "audio" ? "audio/mpeg" : "video/mp4",
287+
"Content-Disposition": `attachment; filename="${encodeURIComponent(
288+
metadata.title || "video"
289+
).replace(/[\u0300-\u036f]/g, "")}.mp4"`,
268290
},
269291
});
270292
} catch (error) {

components/custom/VideoSelect.tsx

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { useEffect, useState } from "react";
44
import { getVideoInfos } from "@/functions/fetchVideoinfos";
55
import { useRouter, useSearchParams } from "next/navigation";
6-
import { YouTubeVideo } from "play-dl";
76
import { Download } from "lucide-react";
87
import {
98
Select,
@@ -13,7 +12,7 @@ import {
1312
SelectValue,
1413
} from "@/components/ui/select";
1514
import clsx from "clsx";
16-
import { formatData } from "@/lib/types";
15+
import { VideoData } from "@/lib/types";
1716
import { VideoLoading } from "./VideoLoading";
1817
import { Progress } from "../ui/progress";
1918

@@ -22,8 +21,10 @@ export const VideoSelect = () => {
2221
const searchParams = useSearchParams();
2322
const videoUrl = searchParams.get("videoUrl");
2423

25-
const [videoData, setVideoData] = useState<YouTubeVideo | null>(null);
26-
const [formats, setFormats] = useState<Partial<formatData>[] | null>(null);
24+
const [videoData, setVideoData] = useState<VideoData["video_details"] | null>(
25+
null
26+
);
27+
const [formats, setFormats] = useState<VideoData["format"] | null>(null);
2728

2829
const [selectedQuality, setSelectedQuality] = useState<string>("audio");
2930

@@ -109,7 +110,7 @@ export const VideoSelect = () => {
109110
return (
110111
<section className="py-8" id="error-search">
111112
<div className="mx-auto my-2 flex h-auto min-h-40 w-11/12 rounded-lg border-2 border-dashed border-[#102F42]">
112-
<p className="m-auto mx-auto text-center font-bold text-red-700 md:text-xl">
113+
<p className="m-auto mx-auto text-center font-bold text-red-800 md:text-xl">
113114
{error ? error : "An error occured"}
114115
</p>
115116
</div>
@@ -120,22 +121,27 @@ export const VideoSelect = () => {
120121
return (
121122
<section className="py-8">
122123
<div className="mx-auto my-2 flex min-h-40 h-auto w-11/12 rounded-lg border-2 border-dashed border-primary py-2 md:py-4 lg:text-xl">
123-
{/* eslint-disable-next-line @next/next/no-img-element */}
124-
<img
125-
// @ts-expect-error -- play-dl is wrongly typed
126-
src={videoData.thumbnail.url}
127-
title={`Thumbnail of ${videoData.title}`}
128-
className="m-auto aspect-video w-3/12 rounded-lg"
129-
alt={`Thumbnail of ${videoData.title}`}
130-
/>
131-
<div className="my-auto flex w-8/12 flex-col">
132-
<h3 className="line-clamp-2">
133-
{videoData.title}{" "}
134-
<span className="font-light italic">
135-
by {videoData.channel?.name}
136-
</span>
137-
</h3>
138-
<div className="mx-2 flex flex-col justify-end md:my-2 md:flex-row">
124+
<div className="w-4/12 mx-2 hidden md:flex">
125+
{/* eslint-disable-next-line @next/next/no-img-element */}
126+
<img
127+
src={videoData.thumbnail}
128+
title={`Thumbnail of ${videoData.title}`}
129+
className="m-auto aspect-video w-full rounded-lg"
130+
alt={`Thumbnail of ${videoData.title}`}
131+
/>
132+
</div>
133+
<div className="my-auto flex w-full md:w-8/12 flex-col">
134+
<h3 className="line-clamp-2 mx-2">{videoData.title}</h3>
135+
<p className="text-base italic text-right mx-2">{videoData.author}</p>
136+
<p className="text-sm font-light italic text-right mx-2">
137+
({videoData.duration} seconds)
138+
</p>
139+
<div
140+
className={clsx(
141+
"mx-2 flex flex-col justify-end md:my-2 md:flex-row",
142+
isDownloading || downloadError ? "hidden" : "flex"
143+
)}
144+
>
139145
<Select
140146
defaultValue={(formats && formats[0].qualityLabel) || "audio"}
141147
onValueChange={(value) => {
@@ -178,7 +184,6 @@ export const VideoSelect = () => {
178184
</SelectItem>
179185
</SelectContent>
180186
</Select>
181-
182187
<button
183188
type="button"
184189
id="download-button"
@@ -232,7 +237,8 @@ export const VideoSelect = () => {
232237
className={clsx(
233238
"mx-2 my-auto flex h-auto flex-col justify-end",
234239
"md:my-2 md:h-10 md:flex-row",
235-
isDownloading || downloadError ? "!h-10" : null
240+
isDownloading || downloadError ? "!h-10" : null,
241+
isDownloading || downloadError ? "flex" : "hidden"
236242
)}
237243
>
238244
{isDownloading && (

functions/fetchVideoinfos.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,39 @@
11
"use server";
22

3-
import { video_info, yt_validate } from "play-dl";
3+
import { FormatData, VideoData } from "@/lib/types";
4+
import ytdl from "@distube/ytdl-core";
45

56
export const getVideoInfos = async (url: string) => {
6-
if (!(url.startsWith("https") && yt_validate(url) === "video")) {
7+
if (!(url.startsWith("https") && ytdl.validateURL(url))) {
78
console.error("Invalid URL");
89
return {
910
error: "Invalid URL",
1011
};
1112
}
1213

13-
const videoData = await video_info(url);
14+
const video = await ytdl.getBasicInfo(url);
15+
16+
const formatMap = new Map();
17+
(video.player_response.streamingData.adaptiveFormats as FormatData[]).forEach(
18+
(format: FormatData) => {
19+
if (!formatMap.has(format.qualityLabel)) {
20+
formatMap.set(format.qualityLabel, format);
21+
}
22+
}
23+
);
24+
25+
const videoData: VideoData = {
26+
video_details: {
27+
title: video.videoDetails.title,
28+
description: video.videoDetails.description || "",
29+
duration: video.videoDetails.lengthSeconds,
30+
thumbnail:
31+
video.videoDetails.thumbnails[video.videoDetails.thumbnails.length - 1]
32+
.url,
33+
author: video.videoDetails.author.name,
34+
},
35+
format: Array.from(formatMap.values()),
36+
};
1437

1538
return JSON.parse(JSON.stringify(videoData));
1639
};

lib/serverUtils.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,26 @@
22
import { execSync } from "child_process";
33
import * as os from "os";
44

5-
export async function detectFfmpegCapabilities() {
5+
type Conf = {
6+
isInitialized: boolean;
7+
ffmpegPath: string;
8+
hasNvidiaCapabilities: boolean;
9+
};
10+
11+
export async function initializeConf(conf: Conf) {
12+
if (conf.isInitialized) {
13+
console.log("Server configuration already initialized.");
14+
return conf;
15+
}
16+
conf.ffmpegPath = await locateFfmpegPath();
17+
conf.hasNvidiaCapabilities = await detectFfmpegCapabilities();
18+
conf.isInitialized = true;
19+
console.log("Server configuration initialized.");
20+
21+
return conf;
22+
}
23+
24+
async function detectFfmpegCapabilities() {
625
if (!(await detectNvidiaGpuAvailable())) {
726
return false;
827
}
@@ -20,7 +39,7 @@ export async function detectFfmpegCapabilities() {
2039
return hasCuda;
2140
}
2241

23-
export async function detectNvidiaGpuAvailable() {
42+
async function detectNvidiaGpuAvailable() {
2443
try {
2544
execSync("nvidia-smi");
2645
return true;
@@ -30,11 +49,7 @@ export async function detectNvidiaGpuAvailable() {
3049
}
3150
}
3251

33-
export async function detectOs() {
34-
return os.platform();
35-
}
36-
37-
export async function locateFfmpegPath() {
52+
async function locateFfmpegPath() {
3853
const detectCommand =
3954
os.platform() === "win32" ? "where ffmpeg" : "which ffmpeg";
4055

lib/types.ts

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
1-
export interface formatData {
1+
export interface FormatData {
22
itag: number;
3-
mimeType: string;
4-
bitrate: number;
5-
width: number;
6-
height: number;
7-
lastModified: string;
8-
contentLength: string;
9-
quality: string;
10-
fps: number;
113
qualityLabel: string;
12-
projectionType: string;
13-
averageBitrate: number;
14-
audioQuality: string;
15-
approxDurationMs: string;
16-
audioSampleRate: string;
17-
audioChannels: number;
18-
url: string;
19-
signatureCipher: string;
20-
cipher: string;
21-
loudnessDb: number;
22-
targetDurationSec: number;
4+
}
5+
6+
export interface VideoData {
7+
video_details: {
8+
title: string;
9+
description: string;
10+
duration: string;
11+
thumbnail: string;
12+
author: string;
13+
};
14+
format: FormatData[];
2315
}

0 commit comments

Comments
 (0)