generate.js final
import fs from "fs";
import path from "path";
import { execSync } from "child_process";
const mediaDir = "./media";
const output = "./data.json";
const files = fs.readdirSync(mediaDir);
const data = [];
function getFileSize(filePath) {
try {
return fs.statSync(filePath).size;
} catch {
return null;
}
}
function getVideoDuration(filePath) {
try {
const result = execSync(
`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${filePath}"`
)
.toString()
.trim();
return Math.round(Number(result));
} catch {
return null;
}
}
files.forEach(file => {
const ext = path.extname(file).toLowerCase();
const name = path.basename(file, ext);
const fullPath = path.join(mediaDir, file);
// VIDEO MP4
if (ext === ".mp4") {
const thumb =
files.find(f => {
const thumbExt = path.extname(f).toLowerCase();
const thumbName = path.basename(f, thumbExt);
return (
thumbName === name &&
[".jpg", ".jpeg", ".png", ".webp"].includes(thumbExt)
);
}) ||
"https://dummyimage.com/300x200/000/fff&text=No+Thumb";
data.push({
title: name,
type: "mp4",
kind: "video",
src: `media/${file}`,
thumb: typeof thumb === "string" && thumb.startsWith("http")
? thumb
: `media/${thumb}`,
duration: getVideoDuration(fullPath),
size: getFileSize(fullPath)
});
}
// M3U8 STREAM
else if (ext === ".m3u8") {
const thumb = files.find(f => {
const thumbExt = path.extname(f).toLowerCase();
const thumbName = path.basename(f, thumbExt);
return (
thumbName === name &&
[".jpg", ".jpeg", ".png", ".webp"].includes(thumbExt)
);
});
data.push({
title: name,
type: "m3u8",
kind: "stream",
src: `media/${file}`,
thumb: thumb
? `media/${thumb}`
: "https://dummyimage.com/300x200/000/fff&text=Stream",
size: getFileSize(fullPath)
});
}
// IMAGE
else if ([".jpg", ".jpeg", ".png", ".webp"].includes(ext)) {
const isUsedAsThumb = files.some(f => {
const mediaExt = path.extname(f).toLowerCase();
const mediaName = path.basename(f, mediaExt);
return mediaName === name && [".mp4", ".m3u8"].includes(mediaExt);
});
if (!isUsedAsThumb) {
data.push({
title: name,
type: "image",
kind: "image",
src: `media/${file}`,
thumb: `media/${file}`,
size: getFileSize(fullPath)
});
}
}
});
fs.writeFileSync(output, JSON.stringify(data, null, 2));
console.log("data.json generated with metadata!");
Kenapa ini sudah benar
Script ini akan:
- scan folder
media - detect:
.mp4.m3u8.jpg/.jpeg/.png/.webp
- untuk video mp4:
- cari thumbnail dengan nama sama
- ambil
duration - ambil
size - set
kind: "video"
- untuk m3u8:
- cari thumbnail dengan nama sama
- ambil
size - set
kind: "stream"
- untuk image:
- kalau image itu bukan thumbnail video/stream, dia masuk sebagai item gallery sendiri
- set
kind: "image"
Contoh hasil data.json
Nanti hasilnya kurang lebih begini:
[
{
"title": "video1",
"type": "mp4",
"kind": "video",
"src": "media/video1.mp4",
"thumb": "media/video1.jpg",
"duration": 125,
"size": 24839291
},
{
"title": "output",
"type": "m3u8",
"kind": "stream",
"src": "media/output.m3u8",
"thumb": "https://dummyimage.com/300x200/000/fff&text=Stream",
"size": 913
},
{
"title": "poster",
"type": "image",
"kind": "image",
"src": "media/poster.jpg",
"thumb": "media/poster.jpg",
"size": 392812
}
]
Jalankan
node generate.js
Kalau ffprobe error
Berarti PATH ffmpeg/ffprobe belum kebaca.
Tapi kalau thumbnail smart tadi sudah jalan, biasanya ffprobe juga ada.
Catatan penting
Kalau kamu punya:
media/video1.mp4
media/video1.jpg
maka video1.jpg dianggap thumbnail, bukan item image terpisah.
Itu memang sengaja, dan itu benar.
Kalau kamu punya image biasa yang ingin tampil di gallery, kasih nama yang tidak sama dengan video:
media/foto-liburan.jpg
Langkah setelah ini
Setelah script ini jalan, pelajaran paling pas adalah:
menampilkan duration dan kind di card gallery supaya data metadata yang sudah kamu generate tidak mubazir.
Kalau mau, next saya bantu rapikan app.js agar card menampilkan label seperti VIDEO, STREAM, IMAGE plus durasi video.