Skip to content

Instantly share code, notes, and snippets.

@isurfer21
Created December 4, 2025 16:17
Show Gist options
  • Select an option

  • Save isurfer21/3ffd6f7bd765a0d87522cc84cbcfef4c to your computer and use it in GitHub Desktop.

Select an option

Save isurfer21/3ffd6f7bd765a0d87522cc84cbcfef4c to your computer and use it in GitHub Desktop.
A CLI tool to generate video list for a given directory to be used with FFmpeg app.
#!/usr/bin/env bun
import { readdir, writeFile, stat } from 'fs/promises';
import { resolve, join, extname } from 'path';
// Define common video file extensions (lowercase)
const VIDEO_EXTENSIONS = new Set([
'.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.3gp'
]);
const OUTPUT_FILE_NAME = 'video_list.txt';
// Interface for storing file data required for sorting
interface FileData {
name: string;
mtimeMs: number; // Last modified time in milliseconds
}
/**
* Natural Sort Comparison function (compares numbers within strings correctly).
*/
function naturalSortCompare(a: string, b: string): number {
const re = /(\d+)/g; // Regex to find sequences of digits
const aParts = a.split(re).filter(Boolean);
const bParts = b.split(re).filter(Boolean);
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
const aPart = aParts[i] || "";
const bPart = bParts[i] || "";
const aIsNum = !isNaN(Number(aPart));
const bIsNum = !isNaN(Number(bPart));
// Case 1: Both parts are numbers
if (aIsNum && bIsNum) {
const numA = Number(aPart);
const numB = Number(bPart);
if (numA !== numB) {
return numA - numB;
}
}
// Case 2: Both parts are strings (or mixed/edge case)
else if (aPart !== bPart) {
if (aPart < bPart) return -1;
if (aPart > bPart) return 1;
}
}
// If all parts were equal, they are identical
return a.length - b.length;
}
/**
* Custom comparison function for sorting.
* 1. Primary sort: Filename using Natural Sort (sequential numeric order).
* 2. Secondary sort: Last Modified Time (mtimeMs) in ascending (oldest first) order.
*/
function customSort(a: FileData, b: FileData): number {
// 1. Sort by filename using Natural Sort
const nameComparison = naturalSortCompare(a.name, b.name);
if (nameComparison !== 0) {
return nameComparison;
}
// 2. If filenames are identical (or naturally equal), sort by updated-datetime (mtimeMs ascending)
if (a.mtimeMs < b.mtimeMs) return -1;
if (a.mtimeMs > b.mtimeMs) return 1;
return 0; // The files are identical in both criteria
}
function showHelp() {
console.log(`Generate Video List (v1.0.0)
Usage:
gen-video-list <directory_path>
bun run gen-video-list.ts <directory_path>
`);
}
async function listSortedVideoFiles() {
// ... (Input validation and path resolution remain the same) ...
const relativePath = process.argv[2];
if (!relativePath) {
console.error('Error: Please provide a directory path as a command-line argument.');
showHelp();
process.exit(1);
}
const absoluteDirPath = resolve(relativePath);
console.log(`\nOriginal Path: ${relativePath}`);
console.log(`Resolved Absolute Directory: ${absoluteDirPath}`);
// ---
let fileDataList: FileData[] = [];
try {
const entries = await readdir(absoluteDirPath, { withFileTypes: true });
for (const entry of entries) {
const fileName = entry.name;
if (fileName === OUTPUT_FILE_NAME || !entry.isFile()) {
continue;
}
const fileExtension = extname(fileName).toLowerCase();
if (VIDEO_EXTENSIONS.has(fileExtension)) {
const fullPath = join(absoluteDirPath, fileName);
try {
const stats = await stat(fullPath);
fileDataList.push({
name: fileName,
mtimeMs: stats.mtimeMs,
});
} catch (statError) {
console.warn(`Warning: Could not get stats for file: ${fileName}. Skipping.`);
}
}
}
console.log(`\nFound ${fileDataList.length} video files.`);
// 3. Sort the Data using the new Natural Sort logic
fileDataList.sort(customSort);
console.log("Files sorted by Filename using Natural (Sequential) order, then by Update Time.");
// 4. Format the Output Content
const fileContent = fileDataList
.map(file => `file '${file.name}'`)
.join('\n') + '\n';
// 5. Write the Prepared List to the Text File (inside the target directory)
const outputFilePath = join(absoluteDirPath, OUTPUT_FILE_NAME);
await writeFile(outputFilePath, fileContent, 'utf-8');
console.log(`Successfully wrote the natural-sorted, formatted list to: ${outputFilePath}`);
} catch (error) {
console.error(`\nAn error occurred while processing the directory: ${absoluteDirPath}`);
if (error instanceof Error) {
console.error(error.message);
}
process.exit(1);
}
}
listSortedVideoFiles();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment