The generated images upstream draws for a folder with no picture of its own #110
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
We draw two of upstream's seven dynamic image providers — the genre collage and the music genre one. The other five leave a folder with no picture where upstream shows a mosaic of what is inside it:
ArtistImageProvider— an artist with no photographCollectionFolderImageProvider— a library's own tile on the home screenDynamicImageProvider(user view) — the same for a viewPlaylistImageProvider— a playlist's tileCollectionImageProvider— a box set's tileAll five subclass
BaseDynamicImageProvider(Emby.Server.Implementations/Images/), which is the code the collage already ports — so this is mostly a matter of which items each one draws from and how many, plus registering them where the collage is registered (crates/jellyfin-server/src/main.rs:734).Separate and more interesting: the three that read the media itself rather than composing other pictures, in
MediaBrowser.Providers/MediaInfo/.AudioImageProvider— the cover art embedded in a track, which is how most music libraries actually carry artworkEmbeddedImageProvider— an attached picture in a video container (mkv cover art)VideoImageProvider— a frame from the video as a primary image where nothing else supplied onescan/images.rssays it in its own module doc: local only — an image already on disk. The prober is explicitly told not to read cover art (crates/jellyfin-ffmpeg/src/prober.rs:288), which is the right call for a probe and means the extraction needs its own pass.Where to start
Two halves, and the second is the interesting one.
The composed five.
ArtistImageProvider,CollectionFolderImageProvider,DynamicImageProvider(user view),PlaylistImageProviderandCollectionImageProviderall subclassBaseDynamicImageProvider(
Emby.Server.Implementations/Images/) — which is the code the genre collagealready ports. So this is mostly which items each one draws from, and how
many, plus registering them where the collage is registered
(
crates/jellyfin-server/src/main.rs:734).The three that read the media.
MediaBrowser.Providers/MediaInfo/:AudioImageProvider(cover art embedded in a track — how most music librariesactually carry artwork),
EmbeddedImageProvider(an attached picture in a videocontainer),
VideoImageProvider(a frame, where nothing else supplied one).crates/jellyfin-app/src/scan/images.rssays in its own module doc that it islocal-only, and the prober is deliberately told not to read cover art
(
crates/jellyfin-ffmpeg/src/prober.rs:288) — correctly, since a cover ismegabytes and a probe is meant to be quick. So the extraction needs a pass of
its own rather than a flag on the probe.
How to know it is done
diff-dbonBaseItemImageInfosover a library both servers scanned, and thefiles compared on bytes. The blurhash comes with them — it is a property of
Skia's resampler, and M10's port is the thing that makes a generated picture's
hash agree at all.
House rules
CLAUDE.mdis the short version and these are the parts that bite: tests rununder
cargo nextest; upstream's source is the checkout inupstream/at thepin (
v12.0-rc5,6d501ba4) and not the clone beside the repository;generated files are regenerated rather than edited; a comment says what the code
does, not how it got there; and
Dnreferences stay out of code and comments.ARCHITECTURE.mddecides which crate a thing goes in and which dependency edgesare illegal,
DECISIONS.mdsays why a rule exists.The reference pair is
cargo xtask reference up(andreference provision,which is idempotent); a run is
cargo run -p jellyfin-harness -- replay --upstream URL --ours URL --scenario FILE --out DIR,with
diff-dbfor the two databases.crates/jellyfin-harness/README.mdsayswhat a diff means and what an ignore entry has to carry — every entry needs a
reason and an expiry milestone, and an adds-only history is the smell the risk
table names.
One of the eight landed in batch D, on
main. Staying open for the other seven,with what was measured about each below so the next pass does not re-derive it.
Landed:
CollectionFolderImageProviderThe tile a library shows on the home screen.
jellyfin-imagesgrew a seconddrawing shape (
Drawn::DimmedbesideDrawn::Quarters),jellyfin-dbgrewlibraries_to_illustrate, andlibraries.rsdraws it where the genre collage isdrawn. Measured on a copy of the
enrichedsnapshot with upstream's two libraryimage rows deleted and our scan re-run:
enriched…/library/e5/e563…/poster.pngenrichedshows…/library/4e/4e4b…/poster.pngSame backdrop, same scaling, same
0x78black wash.The blur hash cannot agree, and this is the finding.
BuildThumbCollageisnot a collage: it takes the first backdrop that decodes, scales it, washes the
canvas with half-opaque black, and then draws the library's name across the
middle at 112 points in
SkiaEncoder.DefaultTypeFace—_typefaces.Last(),whatever the host's fontconfig answers with. Upstream's own
enrichedtile isdominated by the word "enriched" in white, and two upstream installs on different
base images do not draw the same tile either. Matching it means committing a font
and a rasteriser. What this server draws is the picture underneath, and
collage.rsandlibraries.rssay so in their doc comments.Also measured, and both change what the remaining work is:
CollectionFolderImageProviderandDynamicImageProviderpick their sourceswith
OrderBy: Random/Shuffle(). Even without the text, which backdropupstream used is not reproducible. Ours takes a stable order and says so.
ArtistImageProviderdraws nothing at all.GetItemsWithImagesreturnsArray.Empty<BaseItem>(), so an artist with no photograph gets none fromupstream either. There is no work here, only a fact — that is seven remaining,
not eight.
IncludeItemTypesupstream is the absence of a type filter, not afilter that admits nothing (
BaseItemRepository.cs:185, and line 218 thesame way). So
CollectionType.foldersreturning[]fromGetBaseItemKindsForCollectionTypemeans afolderslibrary's tile is drawnfrom everything inside it, not that upstream draws none.
The review round is what made the query upstream's: it now narrows by
GetBaseItemKindsForCollectionTypewith themusic → MusicArtistoverride atthe call site where upstream applies it, requires a candidate to carry a
Primary, and does Backdrop→Primary→Thumb as a per-itemCASErather than aglobal
ORDER BY. Without the type narrowing a tvshows library drew an episodestill where upstream draws the series poster.
crates/jellyfin-db/tests/library_tile.rsis seven cases, one per rule.
A note for whoever writes the next one: the thumbnail branch of
GetStripCollageImagePathsis unreachable for a library tile — a candidatehas to carry a
Primary, so there is always a poster to fall back to before thethumbnail is reached.
The seven, and what each needs
DynamicImageProvider(user view)UserViewUserViewrows — the views it serves are computed. Nothing to hang it on, and the only such row in the fixture is Live TV, which upstream'sSupportsrefuses anywayPlaylistImageProviderCollectionImageProviderPrimary— byte-exact by constructionitem.IsLocked, which this server never sets. It would be dead codeAudioImageProviderEmbeddedImageProvider.mkvcarries anattached_picVideoImageProviderreference/README.mdrecords thatprovisiondeliberately turns the screen grabber off server-wide. Implementing it creates rows the reference, as configured, does not have. Cheap now: it isStills::cutat one offset, and #99'sintervals.rsproved the plumbingThe precondition for the three media readers is a fixture that carries embedded
artwork — growing
make-sample-media.shto attach a cover to the mp3, the flacand one
.mkv. Without it none of them is measurable.The blur hash and the ignore list
No entry was opened for it, and the reason is in
crates/jellyfin-harness/README.mdinstead:Exemptioncannot name rows, onlycolumns, so an entry would hide
Blurhashfor the ~120 fetched pictures whosehashes do agree in order to cover two
CollectionFolderrows — and no run hasproduced the difference, because both snapshots already carry upstream's tiles
and either server leaves a row that has a picture alone. That is filed as #151.
Landed in
915c37f(the composed half) and4ccdb3d(the three that read themedia).
The composed five
Two of the five draw nothing here, and both because upstream draws nothing
either:
ArtistImageProviderasks for no items at all.GetItemsWithImagesreturns
Array.Empty<BaseItem>()at the pin — the query is commented outabove a
TODO— soCreateImageis handed an empty collection and returnsnull. An artist with no photograph keeps none upstream.
DynamicImageProviderdraws for aUserView, and it only supports thethree collection-strip types. This server synthesizes its views from the
library folders (
jellyfin_app::library::views) and files noUserViewrow,so there is nothing for it to run against.
Both are recorded in the note beside the runner rather than left as a gap.
The other two are ported:
PlaylistImageProvider— the same square of quarters a genre gets, drawnfrom the first four items the playlist names.
CollectionImageProvider—CreateSingleImage, so the first picture iscopied whole and keeps its own extension, and only while
IsLocked, whichis upstream's way of keeping a drawn tile from landing ahead of a fetched one.
Each named item offers one picture in upstream's three tries: an episode offers
its series', anything else offers its own, a track with none offers its album's.
linked_rows_to_illustrateis the one read;crates/jellyfin-db/tests/linked_pictures.rsholds the rules.
The three that read the media
crates/jellyfin-app/src/scan/media_images.rsdecides what to take out of afile and where it goes; the taking is a phase of its own in the scan, before the
by-name pictures, because a genre collage is drawn out of what it leaves.
still.rsgrew the two argument shapes upstream'sExtractImagetakes besidethe one it already had: mapping a stream (
-map 0:N, no-ss) rather thanseeking to a frame, and the
isAudiobranch, which skips the keyframe attemptand never passes
-s. Both verified against real ffmpeg.extract_imagesis the library setting. On by default, as upstream's threeare — a library registered with no
TypeOptionsenables them.Two things this leaves
The fixture cannot measure any of it.
xtask reference provisionregistersevery library with empty
ImageFetchersper type, which disables all threeextractors along with the remote ones — deliberately, so a scan is a function of
the files. That is why no item in the default snapshot carries a
Primary. Italso means nothing in the fixture media carries cover art or an attached
picture. Measuring these against a pair takes a library provisioned with the
fetchers on, a track with cover art, and a container with an attached picture;
reference/README.mdnow says so beside the trickplay note, and it belongs with#148's fixture work.
Upstream has three more dynamic providers this issue did not list.
FolderImageProvider,MusicAlbumImageProviderandPhotoAlbumImageProviderall subclass
BaseFolderImageProvider, and all copy the first child's primarypicture the way the box set does. That is the rest of the ten — this issue named
seven — and
MusicAlbumImageProvideris the one that matters in practice, sinceit is what gives an album the cover its first track carries. Filed separately
rather than folded in here.