A playlist's Data blob carries a member upstream ignores and misses one it writes #160

Closed
opened 2026-08-20 20:13:21 +02:00 by Claude · 3 comments
Collaborator

A playlist's Data blob is now written in upstream's declaration order (#150),
but three of its members still disagree with what upstream serializes at the
pin. Sharing::blob in crates/ephyra-core/src/rows/item.rs writes:

{"OwnerUserId":…,"OpenAccess":false,"Shares":[…],"LinkedChildren":[],
 "IsRoot":false,"IsHD":false,"IsShortcut":false,"Width":0,"Height":0,
 "RemoteTrailers":[]}
  • LinkedChildren should not be there. Folder.LinkedChildren is
    [JsonIgnore] at the pin — the links live in the LinkedChildren table, which
    ephyra-db/src/linked.rs already writes — so upstream's blob carries no such
    member. Playlist and BoxSet override FilterLinkedChildrenPerUser and not
    the property, so neither re-serializes it.
  • PlaylistMediaType is missing. Playlist declares it after Shares,
    unignored, so upstream's blob has it where this writes LinkedChildren. What
    a playlist holds is already modelled — Write::playing — but Sharing is not
    told it.
  • OwnerUserId is a Guid, not a Guid?. Upstream writes
    "00000000-0000-0000-0000-000000000000" for a playlist nobody owns; ours
    writes null. Sharing::parse already reads the empty guid as nobody, so only
    the write side differs.

Why it is filed rather than fixed

No fixture holds a playlist row — the reference snapshots have none, so nothing
diffs one — and the second item is not a spelling change: something has to
decide what media type a playlist created through the route is written with, and
where Sharing learns it. That is a decision, not an edit.

How to know it is done

A playlist created through POST /Playlists on both servers leaves the same
BaseItems.Data text once DateLastSaved is masked, and
a_playlists_blob_is_written_in_the_order_upstream_declares_it in
rows/item.rs says what the members are as well as what order they are in.
That takes a container of its own from the pinned image over a cp -a copy of
a snapshot — creating a playlist is a write.

House rules

Tests run under cargo nextest; upstream's source is the checkout in
upstream/ at the pin. The reference on :8096 is read-only. Anything
needing a write gets its own container from the pinned image over a cp -a copy
of a snapshot, on its own ports.

A playlist's `Data` blob is now written in upstream's declaration order (#150), but three of its **members** still disagree with what upstream serializes at the pin. `Sharing::blob` in `crates/ephyra-core/src/rows/item.rs` writes: ``` {"OwnerUserId":…,"OpenAccess":false,"Shares":[…],"LinkedChildren":[], "IsRoot":false,"IsHD":false,"IsShortcut":false,"Width":0,"Height":0, "RemoteTrailers":[]} ``` - **`LinkedChildren` should not be there.** `Folder.LinkedChildren` is `[JsonIgnore]` at the pin — the links live in the `LinkedChildren` table, which `ephyra-db/src/linked.rs` already writes — so upstream's blob carries no such member. `Playlist` and `BoxSet` override `FilterLinkedChildrenPerUser` and not the property, so neither re-serializes it. - **`PlaylistMediaType` is missing.** `Playlist` declares it after `Shares`, unignored, so upstream's blob has it where this writes `LinkedChildren`. What a playlist holds is already modelled — `Write::playing` — but `Sharing` is not told it. - **`OwnerUserId` is a `Guid`, not a `Guid?`.** Upstream writes `"00000000-0000-0000-0000-000000000000"` for a playlist nobody owns; ours writes `null`. `Sharing::parse` already reads the empty guid as nobody, so only the write side differs. ## Why it is filed rather than fixed No fixture holds a playlist row — the reference snapshots have none, so nothing diffs one — and the second item is not a spelling change: something has to decide what media type a playlist created through the route is written with, and where `Sharing` learns it. That is a decision, not an edit. ## How to know it is done A playlist created through `POST /Playlists` on both servers leaves the same `BaseItems.Data` text once `DateLastSaved` is masked, and `a_playlists_blob_is_written_in_the_order_upstream_declares_it` in `rows/item.rs` says what the members are as well as what order they are in. That takes a container of its own from the pinned image over a `cp -a` copy of a snapshot — creating a playlist is a write. ### House rules Tests run under `cargo nextest`; upstream's source is the checkout in `upstream/` **at the pin**. The reference on `:8096` is read-only. Anything needing a write gets its own container from the pinned image over a `cp -a` copy of a snapshot, on its own ports.
Author
Collaborator

Two of the three are fixed; the middle one is what is left, and it is worse
than filed.

OwnerUserId and the guid spelling — fixed (6684877). The severity was
not what it looked like either way: upstream registers a JsonGuidConverter
whose Read maps a null to Guid.Empty, so "OwnerUserId":null was read
correctly and lost nothing. What it was is a difference in the diffed column.
The same converter's Write is value.ToString("N") — unhyphenated — so
every guid in this blob was spelled wrong as well, Shares[].UserId included.
Both now go through to_wire(), and an unowned playlist carries the empty guid.

LinkedChildren / PlaylistMediaType — still open, and it costs a read.
Playlist.MediaType is public override MediaType MediaType => PlaylistMediaType
— a getter with no setter. So a playlist this server wrote and upstream then
opens gets PlaylistMediaType = MediaType.Unknown, the enum default, and the
MediaType column cannot repair it: SupportsPlayedStatus, playback and
client-side filtering all see an untyped playlist. It is not only a text
difference.

The two members sit in the same slot — PlaylistMediaType is declared between
Shares and the inherited members, exactly where LinkedChildren is written
now — so swapping them fixes the text and the read together. What still needs
deciding is where Sharing learns the media type: Write::playing already
models it, and nothing hands it to the blob writer.

Two of the three are fixed; the middle one is what is left, and it is worse than filed. **`OwnerUserId` and the guid spelling — fixed** (`6684877`). The severity was not what it looked like either way: upstream registers a `JsonGuidConverter` whose `Read` maps a null to `Guid.Empty`, so `"OwnerUserId":null` was read correctly and lost nothing. What it *was* is a difference in the diffed column. The same converter's `Write` is `value.ToString("N")` — **unhyphenated** — so every guid in this blob was spelled wrong as well, `Shares[].UserId` included. Both now go through `to_wire()`, and an unowned playlist carries the empty guid. **`LinkedChildren` / `PlaylistMediaType` — still open, and it costs a read.** `Playlist.MediaType` is `public override MediaType MediaType => PlaylistMediaType` — a getter with no setter. So a playlist this server wrote and upstream then opens gets `PlaylistMediaType = MediaType.Unknown`, the enum default, and the `MediaType` column cannot repair it: `SupportsPlayedStatus`, playback and client-side filtering all see an untyped playlist. It is not only a text difference. The two members sit in the same slot — `PlaylistMediaType` is declared between `Shares` and the inherited members, exactly where `LinkedChildren` is written now — so swapping them fixes the text and the read together. What still needs deciding is where `Sharing` learns the media type: `Write::playing` already models it, and nothing hands it to the blob writer.
Author
Collaborator

Fixed in b315fd8. Sharing::blob takes the media type — both call sites
already had it in hand for playing() — and writes PlaylistMediaType in the
slot LinkedChildren occupied, spelled the way JsonStringEnumConverter
spells an enum.

a_playlists_blob_names_the_media_type_and_not_its_children in
crates/ephyra-http/tests/playlists.rs creates a playlist through the route
and reads the stored blob back out of the database, so it fails on either half.

Fixed in `b315fd8`. `Sharing::blob` takes the media type — both call sites already had it in hand for `playing()` — and writes `PlaylistMediaType` in the slot `LinkedChildren` occupied, spelled the way `JsonStringEnumConverter` spells an enum. `a_playlists_blob_names_the_media_type_and_not_its_children` in `crates/ephyra-http/tests/playlists.rs` creates a playlist through the route and reads the stored blob back out of the database, so it fails on either half.
Author
Collaborator

A review of the fix found the member is now load-bearing in a way the port was
not ready for, so 4fb618d follows it.

ephyra_core::item::MediaType carried three of upstream's five — no Photo,
no Book — and every playlist path collapsed the rest to audio. Harmless while
that only reached the MediaType column, which upstream ignores for a
playlist; not harmless once the blob carries the member upstream reads.

  • written() re-derived the value on every edit (_ => Audio). Upstream's
    UpdatePlaylist mutates the loaded playlist and never touches
    PlaylistMediaType, so a playlist it resolved off a [playlist] directory
    with no playlist.xml — Unknown — became an audio one for good on the
    first rename. It reads the stored value now.
  • Creation took only Video and Audio, off the query and off what the
    playlist holds. Upstream keeps any non-Unknown request.MediaType and
    takes item.MediaType straight off the first item, falling back to Audio
    only when nothing said anything — which linked::holding now matches.

an_edit_keeps_a_media_type_this_server_would_not_have_chosen covers the first
and fails on the old written(); the blob test gained a films-only playlist,
since asserting Audio alone could not fail while Audio is also the
nothing-recognised fallback.

A review of the fix found the member is now load-bearing in a way the port was not ready for, so `4fb618d` follows it. `ephyra_core::item::MediaType` carried three of upstream's five — no `Photo`, no `Book` — and every playlist path collapsed the rest to audio. Harmless while that only reached the `MediaType` column, which upstream ignores for a playlist; not harmless once the blob carries the member upstream reads. - **`written()` re-derived the value on every edit** (`_ => Audio`). Upstream's `UpdatePlaylist` mutates the loaded playlist and never touches `PlaylistMediaType`, so a playlist it resolved off a `[playlist]` directory with no `playlist.xml` — `Unknown` — became an audio one for good on the first rename. It reads the stored value now. - **Creation took only `Video` and `Audio`**, off the query and off what the playlist holds. Upstream keeps any non-`Unknown` `request.MediaType` and takes `item.MediaType` straight off the first item, falling back to `Audio` only when nothing said anything — which `linked::holding` now matches. `an_edit_keeps_a_media_type_this_server_would_not_have_chosen` covers the first and fails on the old `written()`; the blob test gained a films-only playlist, since asserting `Audio` alone could not fail while `Audio` is also the nothing-recognised fallback.
Sign in to join this conversation.
No description provided.