1515import threading
1616import time
1717import webbrowser
18+ from datetime import datetime
1819from http import HTTPStatus
1920from http .server import BaseHTTPRequestHandler , ThreadingHTTPServer
2021from pathlib import Path
252253 color: var(--text-muted);
253254 white-space: nowrap;
254255 }
255- table { width: 100%; min-width: 840px ; border-collapse: collapse; table-layout: fixed; }
256+ table { width: 100%; min-width: 1120px ; border-collapse: collapse; table-layout: fixed; }
256257 th {
257258 position: sticky;
258259 top: 0;
546547 <table>
547548 <colgroup>
548549 <col style="width: 46px">
549- <col style="width: 32%">
550+ <col style="width: 25%">
551+ <col style="width: 12%">
552+ <col style="width: 13%">
553+ <col style="width: 8%">
554+ <col style="width: 9%">
550555 <col style="width: 14%">
551- <col style="width: 15%">
552- <col style="width: 11%">
556+ <col style="width: 9%">
553557 <col style="width: 10%">
554- <col style="width: 14%">
555558 </colgroup>
556559 <thead>
557560 <tr>
558- <th class="select-head"></th><th data-i18n="filename">Filename</th><th data-i18n="resolution">Resolution</th><th data-i18n="codec">Codec</th><th>FPS</th><th data-i18n="duration">Dur</th><th data-i18n="status">Status</th>
561+ <th class="select-head"></th><th data-i18n="filename">Filename</th><th data-i18n="resolution">Resolution</th><th data-i18n="codec">Codec</th><th>FPS</th><th data-i18n="duration">Dur</th><th data-i18n="fileTime">Created/Modified</th><th data-i18n="fileSize">Size</th><th data-i18n=" status">Status</th>
559562 </tr>
560563 </thead>
561564 <tbody id="fileRows"></tbody>
646649 const messages = {
647650 en: {
648651 ffmpegNotChecked: "! FFmpeg Not Checked", ffmpegChecking: "... Checking FFmpeg", ffmpegInstalled: "✓ FFmpeg Installed", ffmpegMissing: "! FFmpeg Missing", refreshFfmpeg: "Refresh FFmpeg check",
649- sourceFiles: "Source Files", noFolderSelected: "No folder selected", selectFolder: "Select Folder", filename: "Filename", resolution: "Resolution", codec: "Codec", duration: "Dur", status: "Status",
652+ sourceFiles: "Source Files", noFolderSelected: "No folder selected", selectFolder: "Select Folder", filename: "Filename", resolution: "Resolution", codec: "Codec", duration: "Dur", fileTime: "Created/Modified", fileSize: "Size", status: "Status",
650653 processConsole: "Process Console", configuration: "Configuration", mergeStrategy: "Merge Strategy", outputSettings: "Output Settings", browse: "Browse",
651654 fastMerge: "Fast Merge", optimalMerge: "Optimal Merge", extremeMerge: "Extreme Merge", lossless: "Lossless", smart: "Smart", bruteForce: "Brute Force",
652655 fastDesc: "Stream copy only. Skips incompatible groups.", optimalDesc: "Groups by orientation and transcodes when needed.", extremeDesc: "Normalizes all files into one output.",
695698 },
696699 zh: {
697700 ffmpegNotChecked: "! FFmpeg 未检查", ffmpegChecking: "... 正在检查 FFmpeg", ffmpegInstalled: "✓ FFmpeg 已安装", ffmpegMissing: "! FFmpeg 缺失", refreshFfmpeg: "重新检查 FFmpeg",
698- sourceFiles: "源文件", noFolderSelected: "未选择文件夹", selectFolder: "选择文件夹", filename: "文件名", resolution: "分辨率", codec: "编码", duration: "时长", status: "状态",
701+ sourceFiles: "源文件", noFolderSelected: "未选择文件夹", selectFolder: "选择文件夹", filename: "文件名", resolution: "分辨率", codec: "编码", duration: "时长", fileTime: "创建/修改", fileSize: "大小", status: "状态",
699702 processConsole: "处理控制台", configuration: "配置", mergeStrategy: "合并策略", outputSettings: "输出设置", browse: "浏览",
700703 fastMerge: "快速合并", optimalMerge: "智能合并", extremeMerge: "强制合并", lossless: "无损", smart: "智能", bruteForce: "强制",
701704 fastDesc: "仅使用流复制,跳过不兼容分组。", optimalDesc: "按横竖屏分组,必要时转码。", extremeDesc: "统一所有文件到一个输出。",
963966 Object.entries(by).forEach(([orientation, group]) => {
964967 const w = Math.max(...group.map(file => file.display_width));
965968 const h = Math.max(...group.map(file => file.display_height));
966- rows.insertAdjacentHTML("beforeend", `<tr class="group-row"><td colspan="7 ">${escapeHtml(t("groupLabel", { orientation, size: `${w}x${h}` }))}</td></tr>`);
969+ rows.insertAdjacentHTML("beforeend", `<tr class="group-row"><td colspan="9 ">${escapeHtml(t("groupLabel", { orientation, size: `${w}x${h}` }))}</td></tr>`);
967970 group.forEach(file => {
968971 const cls = file.fast_ready ? "status-ok" : "status-warn";
969972 const status = file.fast_ready ? t("ready") : t("needsTranscode");
970973 const checked = state.selectedPaths.has(file.path) ? "checked" : "";
974+ const timeTitle = `Created: ${file.created_time || "-"} | Modified: ${file.modified_time || "-"} | ${file.path}`;
971975 rows.insertAdjacentHTML("beforeend", `
972976 <tr>
973977 <td class="select-cell"><input class="file-checkbox" type="checkbox" data-path="${escapeHtml(file.path)}" ${checked}></td>
976980 <td class="mono">${escapeHtml(file.video_codec)}/${escapeHtml(file.audio_codec || "none")}</td>
977981 <td class="mono">${escapeHtml(file.fps)}</td>
978982 <td class="mono">${escapeHtml(file.duration)}</td>
983+ <td class="mono" title="${escapeHtml(timeTitle)}">${escapeHtml(file.file_time || "-")}</td>
984+ <td class="mono">${escapeHtml(file.file_size || "-")}</td>
979985 <td class="${cls}">${escapeHtml(status)}</td>
980986 </tr>`);
981987 });
@@ -1547,6 +1553,7 @@ def _serialize_files(files: list[VideoFile]) -> list[dict[str, object]]:
15471553 output = []
15481554 for file in files :
15491555 fast_ready = any (file in members and len (members ) > 1 for members in fast_groups .values ())
1556+ stat_info = _file_stat_info (file .path )
15501557 output .append (
15511558 {
15521559 "path" : str (file .path ),
@@ -1559,11 +1566,50 @@ def _serialize_files(files: list[VideoFile]) -> list[dict[str, object]]:
15591566 "duration" : _format_duration (file .duration ),
15601567 "orientation" : file .orientation .value ,
15611568 "fast_ready" : fast_ready ,
1569+ ** stat_info ,
15621570 }
15631571 )
15641572 return output
15651573
15661574
1575+ def _file_stat_info (path : Path ) -> dict [str , object ]:
1576+ try :
1577+ stat = path .stat ()
1578+ except OSError :
1579+ return {
1580+ "created_time" : "" ,
1581+ "modified_time" : "" ,
1582+ "file_time" : "" ,
1583+ "file_size" : "" ,
1584+ "file_size_bytes" : 0 ,
1585+ }
1586+ created_timestamp = getattr (stat , "st_birthtime" , None )
1587+ created_time = _format_timestamp (created_timestamp ) if created_timestamp else ""
1588+ modified_time = _format_timestamp (stat .st_mtime )
1589+ return {
1590+ "created_time" : created_time ,
1591+ "modified_time" : modified_time ,
1592+ "file_time" : created_time or modified_time ,
1593+ "file_size" : _format_file_size (stat .st_size ),
1594+ "file_size_bytes" : stat .st_size ,
1595+ }
1596+
1597+
1598+ def _format_timestamp (timestamp : float ) -> str :
1599+ return datetime .fromtimestamp (timestamp ).strftime ("%Y-%m-%d %H:%M:%S" )
1600+
1601+
1602+ def _format_file_size (size : int ) -> str :
1603+ value = float (max (size , 0 ))
1604+ units = ("B" , "KB" , "MB" , "GB" , "TB" )
1605+ for unit in units :
1606+ if value < 1024 or unit == units [- 1 ]:
1607+ if unit == "B" :
1608+ return f"{ int (value )} { unit } "
1609+ return f"{ value :.1f} { unit } "
1610+ value /= 1024
1611+
1612+
15671613def _recommended_gpu_mode (gpu_encoders : list [str ]) -> str :
15681614 available = set (gpu_encoders )
15691615 system = platform .system ()
0 commit comments