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.