By-name listings ignore SortBy and SortOrder, and an album with no artist folder borrows from the wrong place #87

Closed
opened 2026-08-12 08:47:04 +02:00 by Claude · 1 comment
Collaborator

Four by-name listings take a SortBy and a SortOrder from the client and answer as if neither had been sent. Both are user-facing controls on those very views, so SortOrder=Descending on /Artists is a button that does nothing.

Measured against the anchor on ee063ca, a pair stood up by ~/.cache/anchor/pair.sh, first three names of ?Limit=5:

route ascending SortOrder=Descending SortBy=DateCreated
/Genres agrees differs differs
/MusicGenres agrees differs agrees here
/Artists agrees differs differs
/Artists/AlbumArtists agrees differs differs
/Studios agrees agrees agrees
/Persons agrees agrees agrees
/Years agrees agrees differs

The bottom three rows are not luck. StudiosController and PersonsController never set OrderBy at all, so ignoring the parameter there is what upstream does — the gap is exactly the four controllers that do pass it:

GenresController      GetOrderBy(sortBy, sortOrder)
MusicGenresController GetOrderBy(sortBy, sortOrder)
ArtistsController     GetOrderBy(sortBy, sortOrder)   // both listings
YearsController       GetOrderBy(sortBy, sortOrder)

/MusicGenres agreeing under SortBy=DateCreated is a coincidence of this library, not support.

Concretely:

GET /Artists/AlbumArtists?Limit=5&SortBy=SortName&SortOrder=Descending
  upstream ['七生報國', 'Various Artists', 'The Vampire Diaries Soundtrack']
  ours     ['?????', 'AES Dana', 'Alestorm']

GET /Artists/AlbumArtists?Limit=5&SortBy=DateCreated
  upstream ['?????', 'AliCukka^^D', 'Carbon Based Lifeforms & Ester Nannmark']
  ours     ['?????', 'AES Dana', 'Alestorm']

Why the sort is not simply routed through the query builder

A by-name listing is not one query. gathered merges the rows a scan wrote with the names no row stands for — derived on the spot, with no DateCreated, no rating and no run time — and sorts the merge in memory. Anything but a name has nothing to sort the derived half on.

/Years shows the shape of the answer: ordered models two of upstream's comparers, applies them stably in reverse, and leaves the order the walk found for any field it does not model. Its own doc is candid that this is upstream's answer only where upstream's comparer holds the items equal — which is why /Years still differs under SortBy=DateCreated, upstream sorting those by when each year's metadata directory was made.

So the work is: give the other four the same treatment, and decide per field whether a derived entry can be ordered by it at all.

A tiebreak that is applied unconditionally

ApplyOrder adds ThenBy(e => e.Name) only when the first ordering is SortName or Default:

if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)

named_in puts BaseItems.Name in its ORDER BY unconditionally, and sort_key carries the name in its key unconditionally. While these listings ignore SortBy that is invisible — the sort they actually do is always by sort name — but it becomes wrong the moment the first is anything else. Whoever fixes the above should make it positional at the same time.

A second residual, on the same fixed page

MusicAlbum.GetMusicArtist walks the album's parents for a MusicArtist and, finding none, falls back on the name:

var name = AlbumArtist;
if (!string.IsNullOrEmpty(name))
{
    return LibraryManager.GetArtist(name, options);
}

So an album with an album artist always resolves to a MusicArtist — an existing by-name row, or one created for the name. Our artist_above walks the parents the same way and then returns nothing, and image_parent drops through to whatever folder encloses the album, lending its pictures.

For a flat music library — tracks and albums with no artist folder above them, and no artist row — upstream inherits from the by-name artist and we inherit from the library folder. It does not reproduce on the anchor, whose albums all sit under artist folders. It was unreachable for an album carrying an album artist until the artist batch started missing on purpose; it is reachable now.

Found while closing the last three of #86.

Four by-name listings take a `SortBy` and a `SortOrder` from the client and answer as if neither had been sent. Both are user-facing controls on those very views, so `SortOrder=Descending` on `/Artists` is a button that does nothing. Measured against the anchor on `ee063ca`, a pair stood up by `~/.cache/anchor/pair.sh`, first three names of `?Limit=5`: | route | ascending | `SortOrder=Descending` | `SortBy=DateCreated` | |---|---|---|---| | `/Genres` | agrees | **differs** | **differs** | | `/MusicGenres` | agrees | **differs** | agrees here | | `/Artists` | agrees | **differs** | **differs** | | `/Artists/AlbumArtists` | agrees | **differs** | **differs** | | `/Studios` | agrees | agrees | agrees | | `/Persons` | agrees | agrees | agrees | | `/Years` | agrees | agrees | **differs** | The bottom three rows are not luck. `StudiosController` and `PersonsController` never set `OrderBy` at all, so ignoring the parameter there is what upstream does — the gap is exactly the four controllers that do pass it: GenresController GetOrderBy(sortBy, sortOrder) MusicGenresController GetOrderBy(sortBy, sortOrder) ArtistsController GetOrderBy(sortBy, sortOrder) // both listings YearsController GetOrderBy(sortBy, sortOrder) `/MusicGenres` agreeing under `SortBy=DateCreated` is a coincidence of this library, not support. Concretely: GET /Artists/AlbumArtists?Limit=5&SortBy=SortName&SortOrder=Descending upstream ['七生報國', 'Various Artists', 'The Vampire Diaries Soundtrack'] ours ['?????', 'AES Dana', 'Alestorm'] GET /Artists/AlbumArtists?Limit=5&SortBy=DateCreated upstream ['?????', 'AliCukka^^D', 'Carbon Based Lifeforms & Ester Nannmark'] ours ['?????', 'AES Dana', 'Alestorm'] ## Why the sort is not simply routed through the query builder A by-name listing is not one query. `gathered` merges the rows a scan wrote with the names no row stands for — derived on the spot, with no `DateCreated`, no rating and no run time — and sorts the merge in memory. Anything but a name has nothing to sort the derived half on. `/Years` shows the shape of the answer: `ordered` models two of upstream's comparers, applies them stably in reverse, and leaves the order the walk found for any field it does not model. Its own doc is candid that this is upstream's answer only where upstream's comparer holds the items equal — which is why `/Years` still differs under `SortBy=DateCreated`, upstream sorting those by when each year's metadata directory was made. So the work is: give the other four the same treatment, and decide per field whether a derived entry can be ordered by it at all. ## A tiebreak that is applied unconditionally `ApplyOrder` adds `ThenBy(e => e.Name)` only when the **first** ordering is `SortName` or `Default`: if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName) `named_in` puts `BaseItems.Name` in its `ORDER BY` unconditionally, and `sort_key` carries the name in its key unconditionally. While these listings ignore `SortBy` that is invisible — the sort they actually do is always by sort name — but it becomes wrong the moment the first is anything else. Whoever fixes the above should make it positional at the same time. ## A second residual, on the same fixed page `MusicAlbum.GetMusicArtist` walks the album's parents for a `MusicArtist` and, finding none, falls back on the name: var name = AlbumArtist; if (!string.IsNullOrEmpty(name)) { return LibraryManager.GetArtist(name, options); } So an album with an album artist always resolves to a `MusicArtist` — an existing by-name row, or one created for the name. Our `artist_above` walks the parents the same way and then returns nothing, and `image_parent` drops through to whatever folder encloses the album, lending *its* pictures. For a flat music library — tracks and albums with no artist folder above them, and no artist row — upstream inherits from the by-name artist and we inherit from the library folder. It does not reproduce on the anchor, whose albums all sit under artist folders. It was unreachable for an album carrying an album artist until the artist batch started missing on purpose; it is reachable now. Found while closing the last three of #86.
Author
Collaborator

Closed. All three, measured against the anchor.

The four controllers that pass an OrderBy now reach the query. /Genres, /MusicGenres, /Artists and /Artists/AlbumArtists take SortBy/SortOrder; /Studios and /Persons still ignore them, which is what upstream does, because their controllers set no ordering at all. Every cell of the table in this issue now agrees — 21 of 21 across the seven routes and the three orderings.

The names no row stands for are merged rather than sorted. A derived entry is a name and nothing else: no date, no rating, no run time, because the row it stands for was never written. So it takes part only in an ordering over a name, and under any other it falls where a row holding nothing in that column falls — first ascending, last descending. The rows themselves keep the order the database gave them.

The Name tiebreak. Upstream adds it behind a leading sort-name ordering and behind no other, and closes with an ascending sort name unless one of the orderings already was a name. It stops there, so rows tied on all of those come back in whatever order its query plan produced; we settle exactly those with the name and then the id, which can only differ where upstream does not decide.

The album borrow was narrower than this issue said. GetImageDisplayParent does fall through to the enclosing folder — but only where the artist lookup returned null, and GetArtist(name) is non-null whenever the album names an album artist. So the rule is not "an album never reaches a folder": an album naming no artist correctly takes what it sits in, and only one naming an artist stops short. That is what landed, with a fixture carrying both cases.

One residual, unchanged and worth stating: where an album names an artist the page does not credit and no artist stands above it, upstream reaches that artist by name and borrows its pictures, while we borrow nothing. Reaching it needs a by-name lookup keyed on the metadata path rather than on the folded or lowercased name, which no batch here holds. The visible difference is confined to an album whose artist has pictures and is neither its parent nor on its page.

portable.json unchanged at 60 responses, 101 differences, 59 agreeing.

Closed. All three, measured against the anchor. **The four controllers that pass an `OrderBy` now reach the query.** `/Genres`, `/MusicGenres`, `/Artists` and `/Artists/AlbumArtists` take `SortBy`/`SortOrder`; `/Studios` and `/Persons` still ignore them, which is what upstream does, because their controllers set no ordering at all. Every cell of the table in this issue now agrees — 21 of 21 across the seven routes and the three orderings. **The names no row stands for are merged rather than sorted.** A derived entry is a name and nothing else: no date, no rating, no run time, because the row it stands for was never written. So it takes part only in an ordering over a name, and under any other it falls where a row holding nothing in that column falls — first ascending, last descending. The rows themselves keep the order the database gave them. **The `Name` tiebreak.** Upstream adds it behind a leading sort-name ordering and behind no other, and closes with an ascending sort name unless one of the orderings already was a name. It stops there, so rows tied on all of those come back in whatever order its query plan produced; we settle exactly those with the name and then the id, which can only differ where upstream does not decide. **The album borrow was narrower than this issue said.** `GetImageDisplayParent` *does* fall through to the enclosing folder — but only where the artist lookup returned null, and `GetArtist(name)` is non-null whenever the album names an album artist. So the rule is not "an album never reaches a folder": an album naming **no** artist correctly takes what it sits in, and only one naming an artist stops short. That is what landed, with a fixture carrying both cases. One residual, unchanged and worth stating: where an album names an artist the page does not credit and no artist stands above it, upstream reaches that artist by name and borrows its pictures, while we borrow nothing. Reaching it needs a by-name lookup keyed on the metadata path rather than on the folded or lowercased name, which no batch here holds. The visible difference is confined to an album whose artist has pictures and is neither its parent nor on its page. `portable.json` unchanged at 60 responses, 101 differences, 59 agreeing.
Sign in to join this conversation.
No description provided.