Instant Mix answers empty everywhere, and the similarity score it was excluded for does not exist #197

Closed
opened 2026-09-12 08:50:58 +02:00 by Claude · 1 comment
Collaborator

Instant Mix is stubbed as an excluded feature, and the reason recorded for the
exclusion is wrong.

// crates/ephyra-http/src/routes/absent.rs:49
// An instant mix is built from a similarity score no provider computes.
.route("/Items/{itemId}/InstantMix", get(no_items_for))
.route("/Albums/{itemId}/InstantMix", get(no_items_for))
.route("/Artists/{itemId}/InstantMix", get(no_items_for))
.route("/Songs/{itemId}/InstantMix", get(no_items_for))
.route("/Playlists/{itemId}/InstantMix", get(no_items_for_a_playlist))
.route("/MusicGenres/{name}/InstantMix", get(no_items_named))
.route("/MusicGenres/InstantMix", get(no_items_for_the_named_id))

All seven answer an empty BaseItemDtoQueryResult, so every Instant mix
button in the music UI does nothing. ENDPOINTS.md counts the tag 7 / 7
answered, which is true of the routes and not of the feature.

There is no similarity score and no provider. Upstream's whole implementation
is upstream/Emby.Server.Implementations/Library/MusicManager.cs, 135 lines,
reaching nothing but its own database:

// MusicManager.cs:88
public IReadOnlyList<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
{
    return _libraryManager.GetItemList(new InternalItemsQuery(user)
    {
        IncludeItemTypes = [BaseItemKind.Audio],
        GenreIds = genreIds,
        Limit = 200,
        OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)],
        DtoOptions = dtoOptions
    });
}

Everything else in the file funnels into that: a song mixes on its own genres
and is put at the head of the result; an album, an artist and a playlist mix on
theirs; a folder mixes on the genres of every Audio beneath it plus its own;
a MusicGenre mixes on its own id. GetInstantMixFromItem (line 99) is the
dispatch and answers an empty list for anything that is none of those.

What this needs here

  • A genre-id query with Limit = 200 and a random sort, which
    crates/ephyra-db/src/query/ already builds every other listing with —
    GenreIds and ItemSortBy.Random both need checking against what the query
    builder spells today.
  • The six item shapes above, dispatched on the row's type. MUSIC_TYPE_NAMES
    in crates/ephyra-core/src/item/kind.rs is the set that has genres.
  • The MusicGenres pair by name and by id, which resolve a by-name row the way
    the other by-name routes in crates/ephyra-http/src/routes/library/ do.
  • Deleting the seven stubs and the comment above them from absent.rs.

Verifying it

Random ordering is the catch: two servers asked the same question answer the
same 200 rows in different orders, and the harness compares envelopes. Worth
settling whether the diff sorts the ids before comparing, or whether the
scenario asks with a seed the query builder does not have. Random is a sort
the query builder may already refuse to make deterministic — check before
writing the scenario, because a scenario that cannot fail is worse than none.

The reference library has a music library with genres on its tracks, so this
needs no new fixture.

Instant Mix is stubbed as an excluded feature, and the reason recorded for the exclusion is wrong. ```rust // crates/ephyra-http/src/routes/absent.rs:49 // An instant mix is built from a similarity score no provider computes. .route("/Items/{itemId}/InstantMix", get(no_items_for)) .route("/Albums/{itemId}/InstantMix", get(no_items_for)) .route("/Artists/{itemId}/InstantMix", get(no_items_for)) .route("/Songs/{itemId}/InstantMix", get(no_items_for)) .route("/Playlists/{itemId}/InstantMix", get(no_items_for_a_playlist)) .route("/MusicGenres/{name}/InstantMix", get(no_items_named)) .route("/MusicGenres/InstantMix", get(no_items_for_the_named_id)) ``` All seven answer an empty `BaseItemDtoQueryResult`, so every *Instant mix* button in the music UI does nothing. `ENDPOINTS.md` counts the tag 7 / 7 answered, which is true of the routes and not of the feature. There is no similarity score and no provider. Upstream's whole implementation is `upstream/Emby.Server.Implementations/Library/MusicManager.cs`, 135 lines, reaching nothing but its own database: ```csharp // MusicManager.cs:88 public IReadOnlyList<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions) { return _libraryManager.GetItemList(new InternalItemsQuery(user) { IncludeItemTypes = [BaseItemKind.Audio], GenreIds = genreIds, Limit = 200, OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)], DtoOptions = dtoOptions }); } ``` Everything else in the file funnels into that: a song mixes on its own genres and is put at the head of the result; an album, an artist and a playlist mix on theirs; a folder mixes on the genres of every `Audio` beneath it plus its own; a `MusicGenre` mixes on its own id. `GetInstantMixFromItem` (line 99) is the dispatch and answers an empty list for anything that is none of those. ## What this needs here - A genre-id query with `Limit = 200` and a random sort, which `crates/ephyra-db/src/query/` already builds every other listing with — `GenreIds` and `ItemSortBy.Random` both need checking against what the query builder spells today. - The six item shapes above, dispatched on the row's type. `MUSIC_TYPE_NAMES` in `crates/ephyra-core/src/item/kind.rs` is the set that has genres. - The `MusicGenres` pair by name and by id, which resolve a by-name row the way the other by-name routes in `crates/ephyra-http/src/routes/library/` do. - Deleting the seven stubs and the comment above them from `absent.rs`. ## Verifying it Random ordering is the catch: two servers asked the same question answer the same 200 rows in different orders, and the harness compares envelopes. Worth settling whether the diff sorts the ids before comparing, or whether the scenario asks with a seed the query builder does not have. `Random` is a sort the query builder may already refuse to make deterministic — check before writing the scenario, because a scenario that cannot fail is worse than none. The reference library has a music library with genres on its tracks, so this needs no new fixture.
Author
Collaborator

Done in 306605c6.

The port is crates/ephyra-http/src/routes/library/instant_mix.rs, and the
seven stubs are gone from absent.rs.

Measured against upstream rather than read out of the C#. Ours and a
pinned upstream were stood up on the same anchor copy and asked the same
questions. Where the two-hundred cap does not bite, the answers are the same
set of rows:

/MusicGenres/InstantMix?id=<acid>        ours 19 rows   upstream 19 rows   same ids
/MusicGenres/InstantMix?id=<acid techno> ours 15        upstream 15        same ids
/MusicGenres/Acid%20Techno/InstantMix    ours 15        upstream 15        same ids

Where it does bite, the counts agree and the samples differ, which is what a
random order over one pool means:

/Songs/{id}/InstantMix            ours total 201   upstream total 201
/Albums/{id}/InstantMix           ours total 200   upstream total 200
/Songs/{id}/InstantMix?limit=3    ours total 201 / 3 shown   upstream total 201 / 3 shown

The 201 is upstream's: the track is prepended after the cap.

Driven through the web client. On the reference fixture, pressing Instant
Mix on an album played:

200 GET  /Items/43e26bc3da945cefca8dc43a77c184c7/InstantMix
206 GET  /Audio/ac63fd705bceba29903119ec9ac738ec/universal
206 GET  /Audio/0e32d313a908c9b790f00af441c79d89/universal

What the review changed

  • An audiobook is a track upstream. AudioBook : Audio, so it takes the
    song branch; the port had matched Audio exactly and answered nothing.
  • Three branches no test could reach — a folder's mix, a playlist's, and the
    no-genre case — now have one each, and deleting the first two fails them.
    The playlist arm and the folder arm compute the same thing here, because
    ancestor_id does not follow LinkedChildren, so no test separates them.
  • ENDPOINTS.md regenerated; the seven rows said absent.

One note for whoever writes the remote-search routes

The exclusion comment above the RemoteImages stubs — the remote metadata
providers, which nothing here can reach
— is as stale as this one was. M10
landed the providers. See #199 and #200.

Done in `306605c6`. The port is `crates/ephyra-http/src/routes/library/instant_mix.rs`, and the seven stubs are gone from `absent.rs`. **Measured against upstream rather than read out of the C#.** Ours and a pinned upstream were stood up on the same anchor copy and asked the same questions. Where the two-hundred cap does not bite, the answers are the same set of rows: ``` /MusicGenres/InstantMix?id=<acid> ours 19 rows upstream 19 rows same ids /MusicGenres/InstantMix?id=<acid techno> ours 15 upstream 15 same ids /MusicGenres/Acid%20Techno/InstantMix ours 15 upstream 15 same ids ``` Where it does bite, the counts agree and the samples differ, which is what a random order over one pool means: ``` /Songs/{id}/InstantMix ours total 201 upstream total 201 /Albums/{id}/InstantMix ours total 200 upstream total 200 /Songs/{id}/InstantMix?limit=3 ours total 201 / 3 shown upstream total 201 / 3 shown ``` The 201 is upstream's: the track is prepended *after* the cap. **Driven through the web client.** On the reference fixture, pressing Instant Mix on an album played: ``` 200 GET /Items/43e26bc3da945cefca8dc43a77c184c7/InstantMix 206 GET /Audio/ac63fd705bceba29903119ec9ac738ec/universal 206 GET /Audio/0e32d313a908c9b790f00af441c79d89/universal ``` ### What the review changed - **An audiobook is a track upstream.** `AudioBook : Audio`, so it takes the song branch; the port had matched `Audio` exactly and answered nothing. - Three branches no test could reach — a folder's mix, a playlist's, and the no-genre case — now have one each, and deleting the first two fails them. The playlist arm and the folder arm compute the same thing here, because `ancestor_id` does not follow `LinkedChildren`, so no test separates them. - `ENDPOINTS.md` regenerated; the seven rows said `absent`. ### One note for whoever writes the remote-search routes The exclusion comment above the `RemoteImages` stubs — *the remote metadata providers, which nothing here can reach* — is as stale as this one was. M10 landed the providers. See #199 and #200.
Sign in to join this conversation.
No description provided.