A by-name item's id depends on where the metadata directory sits #67

Closed
opened 2026-08-04 13:58:17 +02:00 by Claude · 1 comment
Collaborator

A by-name item's id depends on where the metadata directory sits.

/Persons against a production library: upstream lists 6 people, this server
lists 99, and none of the ids agree — upstream's 2 Chainz is
9bca90d85c196e4aff33de16eb67bb88, ours is a7dbc42606c1dd92df8f38fe4e33f766.
Both rows exist in the same database, with the same Path.

GetNewItemIdInternal (LibraryManager.cs:782) makes the path relative to the
program data directory
before hashing — but only when it is under it. The
reference container bind-mounts its metadata directory outside its
ProgramDataPath, so upstream hashes the absolute path there and this port
hashes the relative form unconditionally.

The database holds both generations: the ids the production install wrote in
2024 (relative, which we reproduce) and the ids the container's own validators
wrote in 2026 (absolute, which upstream now finds). GetPerson looks a name up
and never creates one, so upstream sees only the 6 it wrote itself.

So this is not a hashing bug — it is a missing condition. Matching upstream means
deciding the form the way it does, from whether the metadata directory lies under
the data directory.

Accounts for 20 of the 23 differences the bundle scenario reports. It applies
to genres, studios and artists as well as people; people are simply where the
two generations differ.

A by-name item's id depends on where the metadata directory sits. `/Persons` against a production library: upstream lists 6 people, this server lists 99, and none of the ids agree — upstream's `2 Chainz` is `9bca90d85c196e4aff33de16eb67bb88`, ours is `a7dbc42606c1dd92df8f38fe4e33f766`. Both rows exist in the same database, with the same `Path`. `GetNewItemIdInternal` (`LibraryManager.cs:782`) makes the path **relative to the program data directory** before hashing — but only when it is under it. The reference container bind-mounts its metadata directory outside its `ProgramDataPath`, so upstream hashes the absolute path there and this port hashes the relative form unconditionally. The database holds both generations: the ids the production install wrote in 2024 (relative, which we reproduce) and the ids the container's own validators wrote in 2026 (absolute, which upstream now finds). `GetPerson` looks a name up and never creates one, so upstream sees only the 6 it wrote itself. So this is not a hashing bug — it is a missing condition. Matching upstream means deciding the form the way it does, from whether the metadata directory lies under the data directory. Accounts for 20 of the 23 differences the `bundle` scenario reports. It applies to genres, studios and artists as well as people; people are simply where the two generations differ.
Author
Collaborator

Fixed. The id now follows where the two directories sit, as upstream's does.

GetNewItemIdInternal (LibraryManager.cs:782) strips the program data
directory only when the key starts with it, as a raw ordinal prefix — then trims
a leading separator and turns every / into \. An absolute key keeps its
forward slashes, and that asymmetry is load-bearing. A by-name key is always
lowercased (EnableNormalizedItemByNameIds, default true), the prefix is the
.NET type's full name, and the hash is MD5 over UTF-16LE read as a little-endian
guid.

Both forms reproduce the answer key exactly:

Person + /home/alex/.cache/anchor/metadata/people/2/2 chainz → 9bca90d8…
Person + metadata\people\2\2 chainz                          → a7dbc426…

The two databases cannot both be matched, and that is the finding. The
production database holds two generations — rows the 2024 install wrote with
metadata inside its data directory (relative form) and rows the container's own
validators wrote in 2026 with metadata bind-mounted outside it (absolute).
MusicGenre splits almost exactly in half between them. Agreement with the
container-laid-out database therefore falls from 97.82 % to 2.18 %, while
agreement with var/big-upstream — metadata inside /config — holds at
99.82 % before and after.

What matters is the running server, and there the harness settles it: the
bundle scenario went from 23 differences to 0, the 20 /Persons lines
among them included.

jellyfin-core learns nothing about configuration; it takes the two paths as
strings, exactly as the existing item-id derivation already did.

One caveat worth recording: a config whose paths.data names the data
subdirectory rather than the program data directory now selects the absolute
form and disagrees with an upstream-laid-out install. The shipped default is
correct; a scratch config may not be.

Fixed. The id now follows where the two directories sit, as upstream's does. `GetNewItemIdInternal` (`LibraryManager.cs:782`) strips the program data directory only when the key starts with it, as a raw ordinal prefix — then trims a leading separator and turns every `/` into `\`. An absolute key keeps its forward slashes, and that asymmetry is load-bearing. A by-name key is always lowercased (`EnableNormalizedItemByNameIds`, default true), the prefix is the .NET type's full name, and the hash is MD5 over UTF-16LE read as a little-endian guid. Both forms reproduce the answer key exactly: Person + /home/alex/.cache/anchor/metadata/people/2/2 chainz → 9bca90d8… Person + metadata\people\2\2 chainz → a7dbc426… **The two databases cannot both be matched, and that is the finding.** The production database holds two generations — rows the 2024 install wrote with metadata inside its data directory (relative form) and rows the container's own validators wrote in 2026 with metadata bind-mounted outside it (absolute). `MusicGenre` splits almost exactly in half between them. Agreement with the *container-laid-out* database therefore falls from 97.82 % to 2.18 %, while agreement with `var/big-upstream` — metadata inside `/config` — holds at 99.82 % before and after. What matters is the running server, and there the harness settles it: the `bundle` scenario went from **23 differences to 0**, the 20 `/Persons` lines among them included. `jellyfin-core` learns nothing about configuration; it takes the two paths as strings, exactly as the existing item-id derivation already did. One caveat worth recording: a config whose `paths.data` names the `data` *subdirectory* rather than the program data directory now selects the absolute form and disagrees with an upstream-laid-out install. The shipped default is correct; a scratch config may not be.
Sign in to join this conversation.
No description provided.