Created
December 4, 2025 16:17
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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