Skip to content

feat(thumbs): video thumbnails, .mkv/.mov by default (v0.13.0) - #12

Merged
agarzon merged 2 commits into
mainfrom
feat/video-thumbnails
Aug 10, 2026
Merged

feat(thumbs): video thumbnails, .mkv/.mov by default (v0.13.0)#12
agarzon merged 2 commits into
mainfrom
feat/video-thumbnails

Conversation

@agarzon

@agarzon agarzon commented Aug 10, 2026

Copy link
Copy Markdown
Owner

What

Video files got a generic 🎬 icon in the grid. They now get a real thumbnail.

The thumbnail pipeline β€” WebP cache, mtime keying, eviction, prune, the HTTP route, the icon fallback β€” was already container-agnostic. Only the decoder was image-only, so this swaps in a PyAV frame grab and reuses everything downstream.

  • Mid-clip frame, not frame 0. A clip that fades in from black gives a featureless rectangle at frame 0. VIDEO_FRAME_POSITION = 0.5, a module constant rather than a config key β€” it would have to join the cache-path filename to be tunable, and nobody tunes this.
  • The seek is guarded. An unseekable container settles for frame 0 rather than losing its thumbnail entirely.
  • No new dependency. PyAV ships with ComfyUI and metadata.py already uses it. Installs without it degrade to the old icon via the existing onerror.
  • .mkv/.mov are video by default now. VideoHelperSuite writes both, and metadata.py already read workflows out of them, but they showed as anonymous πŸ“„ files with no thumbnail and no preview unless files.video_extensions was hand-written.
  • Unplayable containers say so. Thumbnails decode server-side and always work; inline playback depends on the browser (Firefox has no Matroska support). The preview pane now falls back to a "download it to view" message instead of a silent blank player.

Verification

Unit tests fake av the way tests/test_metadata.py does, so the suite stays PyAV-free: midpoint seek offset, unseekable fallback, missing-PyAV, and a route test that the extension gate reaches the decoder with video=True.

Fakes can lie, so this was also run end to end against the real thing β€” ComfyUI in Docker with PyAV 18.0.0, driving the actual UI:

  • A 6.7 MB H.264/AAC clip: HTTP 200 in 71 ms, 320Γ—177 WebP, mid-clip frame. Cached hit: 2.7 ms.
  • A synthetic clip that is black for its first third: the thumbnail is frame 20 β€” the keyframe at or before the 3 s midpoint β€” proving the seek does what it exists for.
  • An iPhone-style .mov carrying three extra data streams alongside video/audio: decodes fine in 80 ms; streams.video[0] picks the right one.
  • Remuxed the same H.264/AAC payload into .mp4/.mkv/.mov so the container was the only variable, and loaded all three through /preview in Chromium: all play. canPlayType("video/quicktime") returns "" yet the file plays β€” Chromium sniffs the container.
  • The fallback message was verified against a real undecodable file (MPEG-4 Part 2), not a mock.

pytest: 297 passed, 1 skipped (pre-existing, platform-specific). node --test: 3/3.

Also

README.md's roots[] row claimed the field was optional with an auto-mount default. Auto-mounting only applies when there is no config.json β€” a config file that omits roots logs a warning and leaves the UI empty (config.py:100). Hit firsthand while testing.

Not included

A β–Ά badge on video tiles. They're now visually identical to image tiles; only the filename distinguishes them.

Video files rendered a generic clapperboard icon in the grid: the whole
thumbnail pipeline β€” WebP cache, mtime keying, eviction, prune β€” was
already container-agnostic, only the decoder was image-only.

Grab a frame with PyAV (a ComfyUI core dependency, already used by
metadata.py) and feed it to the existing encode path. Seek to the
midpoint rather than frame 0, which on a clip that fades in from black
is a featureless rectangle. The seek is guarded: an unseekable
container settles for frame 0 instead of losing its thumbnail.

Also promote .mkv/.mov to video by default. VideoHelperSuite writes
both and metadata.py already reads workflows out of them, but they
showed as anonymous files with no thumbnail and no preview unless
files.video_extensions was written by hand. Thumbnails decode server
side so they always work; inline playback depends on the browser, so
the preview pane now falls back to a "download it to view" message
instead of a silent blank player.

Fixes the README's roots[] row while in there: it advertised an
auto-mount default that only applies when no config.json exists at all.
Copilot AI lite review requested due to automatic review settings August 10, 2026 14:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds first-class video thumbnails to FileManaty by decoding a representative mid-clip frame via PyAV while reusing the existing WebP thumbnail cache and HTTP thumbnail route. It also expands default video extension handling (notably .mkv/.mov) and improves the preview UX when a browser cannot decode a container.

Changes:

  • Add PyAV-backed video frame extraction to the thumbnail generator and plumb a video flag through the thumbnail API route.
  • Treat .mkv and .mov as video by default (config defaults + example config + tests).
  • Update the frontend to request thumbnails for video entries and show a clear fallback message when inline playback fails; refresh docs/changelog/version accordingly.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
web/filemanaty.js Requests thumbnails for video tiles and adds a playback-error fallback message in the preview pane.
filemanaty/thumbs.py Implements video-frame decoding via PyAV and routes it through the existing WebP thumbnail pipeline.
filemanaty/api.py Extends the thumbnail endpoint to allow video extensions and invoke the decoder with video=True.
filemanaty/config.py Expands default video extensions to include .mkv and .mov.
tests/test_thumbs.py Adds unit coverage for midpoint seek, unseekable fallback, and missing-PyAV behavior.
tests/test_api.py Verifies video extensions pass the route gate and reach the decoder with video=True.
tests/test_config.py Updates expectations for default video extensions after config fallback.
README.md Documents video thumbnails and clarifies roots[] behavior once config.json exists.
config.example.json Updates example config to include .mkv/.mov in video_extensions.
CHANGELOG.md Adds v0.13.0 release notes for video thumbnails, defaults, and preview fallback message.
filemanaty/init.py Bumps package version to 0.13.0.

πŸ’‘ Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread filemanaty/thumbs.py Outdated
Comment thread web/filemanaty.js
Comment thread filemanaty/thumbs.py
…essage

From Copilot review on #12:

- `generate_thumbnail` reported every failure as "cannot read image".
  PyAV raises OSError subclasses, so a bad .mp4 logged as an image
  problem and sent log readers hunting for a corrupt PNG.
- The preview fallback claimed "can't play this format", but the media
  error event also fires on network and decode failures. Reworded to
  cover any failure β€” the advice (download it) holds either way.
- Dropped the `ponytail:` marker on VIDEO_FRAME_POSITION; that prefix is
  a convention from my tooling, not this repo's, and appears nowhere
  else here. Same content, no jargon.
@agarzon

agarzon commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

Addressed in 6aa612b.

cannot read image on video failures β€” valid, fixed. PyAV raises OSError subclasses, so a bad .mp4 was logged as an image problem, which sends log readers hunting for a corrupt PNG. The message now names the kind actually being decoded.

Player error message β€” valid, fixed. The error event does fire for network and decode failures, not just unsupported containers. Reworded to "Can't play this video in the browser β€” download it to view", which is true for any of those, and the advice is the same in every case.

ponytail: prefix β€” valid, dropped. That marker comes from my tooling, not this repo; it appears nowhere else here, so it read as a stray note exactly as described. Same content, no jargon.

Null-check on #fm-media β€” declined. The element is created by the innerHTML assignment three lines above and #fm-preview is already assumed to be in the document by this same function (it does el.innerHTML at the top of renderPreview with no guard). Adding a guard on one of two sibling lookups would be inconsistent without making anything safer.

@agarzon
agarzon merged commit 8fd5cdb into main Aug 10, 2026
1 check passed
@agarzon
agarzon deleted the feat/video-thumbnails branch August 10, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants