Skip to content

feat: use the video's own cover instead of a grabbed frame - #34

Merged
houko merged 1 commit into
mainfrom
feat/source-cover
Nov 9, 2026
Merged

houko merged 1 commit into
mainfrom
feat/source-cover

Conversation

@houko

@houko houko commented Nov 9, 2026

Copy link
Copy Markdown
Member

Summary

Library cards, the hero and each film's cover.jpg showed a frame ffmpeg grabbed from the downloaded video, even though the site had already given the video a cover (it was used on the processing card and then replaced once the download finished). Posters now come from the video itself:

  1. A film from a site: the picture the site gives it (job.Thumbnail, already saved in library.json). It is fetched once into the posters cache, which the processing card's thumbnail already uses, so a film that just finished downloading usually needs no new request.
  2. A local video: the cover art its file carries (an MP4/MKV attached picture), read with ffmpeg -map 0:v -map -0:V -c copy (new media.EmbeddedCover).
  3. Fallback: only a video with neither keeps the cover.jpg it already has, or has a frame grabbed as before. This covers a site that no longer serves the picture (for example TikTok's signed thumbnail URLs expire) or a machine that is offline.

cover.jpg is rewritten whenever its bytes differ from the video's own cover, so existing libraries swap their grabbed frames for the site's picture on the next load. The rewrite is idempotent and needs no new field in library.json. Non-JPEG pictures (YouTube's WebP thumbnails, PNG cover art) are re-encoded as JPEG so cover.jpg stays a JPEG. That needs the golang.org/x/image/webp decoder, which apps/kando already had as an indirect dependency through MyGo; go.mod only moves it to the direct block, at the same version v0.47.0.

Screenshots

The hero of the screenshot fixture, before (a frame grabbed from the video) and after (the site's picture; the fixture stands in for the site with its generic scene at 1920 px, so the difference is visible offline):

before: grabbed frame
after: the site's cover

Test plan

  • CGO_ENABLED=0 go test ./packages/core/... ./packages/media/... ./apps/kando/... passes
  • go tool mygo vet . in apps/kando is clean
  • New tests:
    • TestEmbeddedCover (real ffmpeg): cover art comes back byte for byte, and a video without cover art has none
    • TestPosterIsTheSitesCover: a grabbed cover.jpg is replaced by the site's PNG, saved as JPEG; the picture is fetched once and kept in the cache; finding it again does not rewrite cover.jpg
    • TestPosterKeepsItsCoverWhenTheSiteHasNone: a failed fetch leaves cover.jpg untouched
    • TestPosterOfALocalVideoIsItsCoverArt
    • TestCoverJPEG: WebP becomes JPEG, JPEG stays as is, and an HTML error page is rejected
  • KANDO_SCREENSHOTS=… go test -run TestScreenshotsLibrary renders the new library-site-cover scene
  • Not checked in the running app against a real download.

A film's poster and cover.jpg now come from the video itself: the picture its site gives it (job.Thumbnail), else the cover art its file carries. A frame is grabbed from the video only when it has neither.

cover.jpg is rewritten whenever it holds something else, so libraries from an earlier Kando swap their grabbed frames for the site's picture the next time the shelf loads. Non-JPEG pictures (YouTube's WebP thumbnails, PNG cover art) are encoded as JPEG so cover.jpg stays a JPEG.
@github-actions github-actions Bot added area: app The app shell and state in apps/kando area: media The media pipeline (packages/media) area: dependencies go.mod, go.sum and go.work files feature A new feature (feat) size/M 100 to 499 changed lines labels Nov 9, 2026
@houko
houko merged commit c98344f into main Nov 9, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: app The app shell and state in apps/kando area: dependencies go.mod, go.sum and go.work files area: media The media pipeline (packages/media) feature A new feature (feat) size/M 100 to 499 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant