Show course runtime on library cards via ffprobe

Add ffmpeg to the Docker image so ffprobe can read each video/audio
file's duration server-side. Results are cached to a small persistent
per-course JSON file keyed by (relative path, size, mtime), so a file
only gets probed once - a container restart or the library scan's
in-memory cache expiring never re-runs ffprobe on unchanged files.

Course cards in the library browser (grid, list, and search) now show
total runtime alongside the media-file count, so you know the time
commitment before opening a course. Durations under a minute are
suppressed rather than showing a noisy "0m".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 22:03:04 -04:00
co-authored by Claude Sonnet 5
parent eae6b09ecd
commit 8f7808ab46
3 changed files with 105 additions and 8 deletions
+5 -2
View File
@@ -1644,12 +1644,15 @@
const progressBar = pct
? `<div class="grid-card-progress-track"><div class="grid-card-progress-fill" style="width: ${pct}%;"></div></div>`
: '';
const metaBits = [`${item.media_files} media file${item.media_files === 1 ? '' : 's'}`];
if (item.duration_display) metaBits.push(item.duration_display);
if (pct) metaBits.push(`${pct}% done`);
return `
<div class="library-grid-card" onclick="${clickHandler}">
${selectableAttrs(item)}
<div class="grid-card-thumb-wrap">${iconHtml}</div>
<div class="grid-card-name" title="${escapeAttr(item.name)}">${item.name}</div>
<div class="grid-card-meta">${item.media_files} media file${item.media_files === 1 ? '' : 's'}${pct ? ` · ${pct}% done` : ''}</div>
<div class="grid-card-meta">${metaBits.join(' · ')}</div>
${progressBar}
</div>
`;
@@ -1688,7 +1691,7 @@
</div>
<div class="lesson-meta">
${progressBadge}
<span>${item.media_files} media file${item.media_files === 1 ? '' : 's'}</span>
<span>${item.media_files} media file${item.media_files === 1 ? '' : 's'}${item.duration_display ? ` · ${item.duration_display}` : ''}</span>
<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); queueCourse('${safePath}', this)" title="Add to Next Up">${ICON_SVGS.pin}</button>
</div>
${progressBar}