Skip to content

Add comprehensive sort field infrastructure - #5498

Draft
dmoo500 wants to merge 9 commits into
music-assistant:devfrom
dmoo500:feat/sort-field-definitions
Draft

Add comprehensive sort field infrastructure#5498
dmoo500 wants to merge 9 commits into
music-assistant:devfrom
dmoo500:feat/sort-field-definitions

Conversation

@dmoo500

@dmoo500 dmoo500 commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

What does this implement/fix?

Implements comprehensive backend infrastructure for improved library sorting, including media-type specific sort logic and backward compatibility.

Depends on: music-assistant/models#356 (must be merged first)

Related discussions:

Changes

Base Controller (base.py)

  • BASE_SORT_FIELD_SQL: Maps SortField enum to SQL expressions for common fields (ARTIST_NAME intentionally omitted - implemented per MediaType in subclasses)
  • LEGACY_SORT_KEYS: Backward compatibility mapping for old "field_desc" format
  • get_sort_options(): New API endpoint returning list[SortOptionInfo] for the MediaType
  • _parse_order_by(): Parses both new "field:direction" and legacy "field_desc" formats
  • _get_sort_sql(): Translates SortField + SortDirection to SQL ORDER BY clause (can be overridden)
  • Modified _build_final_query(): Uses new parsing logic

Media-Type Specific Controllers

TracksController (tracks.py)

  • _get_sort_sql() override: For ARTIST_NAME returns "artists.search_name ASC/DESC, tracks.search_name ASC" (secondary sort on track name)
  • library_items() modified: Detects ARTIST_NAME sorting and adds artist JOIN

AlbumsController (albums.py)

  • _get_sort_sql() override: For ARTIST_NAME returns "artists.search_name ASC/DESC, year DESC" (secondary sort on year)
  • library_items() modified: Detects ARTIST_NAME sorting and adds artist JOIN

Format

  • New: "field:direction" (e.g., "name:asc", "timestamp_added:desc")
  • Legacy: "field_desc" (e.g., "name_desc", "timestamp_added")
  • RANDOM and RANDOM_PLAY_COUNT don't support direction

Types of changes

  • New feature (non-breaking change which adds functionality) — new-feature
  • Enhancement to an existing feature — enhancement

Checklist

  • The code change is tested and works locally. (Syntax validated, will runtime-test after models merge)
  • pre-commit run --all-files passes. (Currently fails on missing models imports - will pass after rebase on models#356)
  • pytest passes, and tests have been added/updated under tests/ where applicable. (No new tests needed - refactors existing sort logic)
  • For changes to shared models, the companion PR in music-assistant/models is linked.
  • For changes affecting the UI, the companion PR in music-assistant/frontend is linked. (Frontend PR to follow)
  • I have read and complied with the project's AI Policy for any AI-assisted contributions.
  • I have raised a PR against the documentation repository targeting the main or beta branch as appropriate. (Not needed - backend infrastructure only)

Copilot AI lite review requested due to automatic review settings August 8, 2026 08:35
@musicassistant-bot

musicassistant-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

✅ The title and description are good to go. Thanks!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new backend sorting infrastructure for library listings by introducing typed sort fields/directions, legacy compatibility parsing, and media-type-specific SQL ordering overrides (notably ARTIST_NAME for tracks/albums).

Changes:

  • Replaces string-based SORT_KEYS handling with SortField/SortDirection parsing via _parse_order_by() and SQL generation via _get_sort_sql().
  • Adds a new API endpoint music/<mediatype>/get_sort_options to expose supported sort options to clients.
  • Implements media-type-specific ARTIST_NAME ordering and conditional artist joins in TracksController and AlbumsController.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
music_assistant/controllers/music/media/base.py Introduces the new sort parsing/SQL infrastructure and registers the new get_sort_options API endpoint.
music_assistant/controllers/music/media/tracks.py Adds track-specific ARTIST_NAME ordering and joins artists when needed for sorting.
music_assistant/controllers/music/media/albums.py Adds album-specific ARTIST_NAME ordering and joins artists when needed for sorting.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread music_assistant/controllers/music/media/base.py Outdated
Comment thread music_assistant/controllers/music/media/base.py
Comment thread music_assistant/controllers/music/media/tracks.py Outdated
Comment thread music_assistant/controllers/music/media/albums.py Outdated
Comment thread music_assistant/controllers/music/media/tracks.py
Copilot AI review requested due to automatic review settings August 8, 2026 09:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (5)

music_assistant/controllers/music/media/base.py:422

  • [CRITICAL] get_sort_options() calls get_sort_options_for_media_type and references SortOptionInfo, but neither symbol is imported anywhere in this module, so calling this endpoint will raise NameError at runtime.
        return get_sort_options_for_media_type(self.media_type)

music_assistant/controllers/music/media/tracks.py:290

  • [CRITICAL] Adding an unconditional track_artists/artists JOIN for ARTIST_NAME sorting can lead to the same tables being joined a second time later in this method (the secondary artist-search query path appends another JOIN), which will break the generated SQL when both paths are hit (e.g. search term + ARTIST_NAME sort).
        if order_by:
            parsed = self._parse_order_by(order_by)
            if parsed and parsed[0] == SortField.ARTIST_NAME:
                extra_join_parts.append(
                    "JOIN track_artists ON track_artists.track_id = tracks.item_id "

music_assistant/controllers/music/media/base.py:1753

  • [PROBLEM] _parse_order_by adds a new public API surface (field:direction + default direction rules) but there are no tests covering the new formats/edge cases (e.g. "name:desc", invalid field, invalid direction, random fields with direction). This logic is central to library listing and regressions here will be hard to spot without automated coverage.
    def _parse_order_by(
        self, order_by: str | None
    ) -> tuple[SortField, SortDirection | None] | None:
        """
        Parse order_by string into SortField and SortDirection.

music_assistant/controllers/music/media/tracks.py:902

  • [PROBLEM] Sorting by ARTIST_NAME orders by artists.search_name while the final query adds GROUP BY tracks.item_id whenever joins are present; for tracks with multiple artists this makes the ORDER BY value effectively arbitrary/non-deterministic. Using an aggregate for the artist sort key (or a correlated subquery) makes ordering stable.
        if field == SortField.ARTIST_NAME:
            if direction == SortDirection.DESC:
                return "artists.search_name DESC, tracks.search_name ASC"
            return "artists.search_name ASC, tracks.search_name ASC"
        return super()._get_sort_sql(field, direction)

music_assistant/controllers/music/media/albums.py:733

  • [PROBLEM] Sorting by ARTIST_NAME orders by artists.search_name while the final query adds GROUP BY albums.item_id whenever joins are present; for albums with multiple artists this makes the ORDER BY value effectively arbitrary/non-deterministic. Using an aggregate for the artist sort key (or a correlated subquery) makes ordering stable.
        if field == SortField.ARTIST_NAME:
            if direction == SortDirection.DESC:
                return "artists.search_name DESC, year DESC"
            return "artists.search_name ASC, year DESC"
        return super()._get_sort_sql(field, direction)

Comment thread music_assistant/controllers/music/media/albums.py Outdated
@marcelveldt

Copy link
Copy Markdown
Member

In general this approach is good and what I had in mind.
Next step (after adjusting the library endpoints/commands) would be to also adjust the other listings, such as playlist tracks etc. and let the server deal with sorting and filtering (such as search) so each client doesnt have to reinvent the wheel but that should be done as a follow-up.

The remarks I have is that the models now leak some server logic, let's not do that and I proposed another way to implement these new fields where they are are fully typed while keeping backwards compatibility.

Copilot AI review requested due to automatic review settings August 10, 2026 06:26
@dmoo500
dmoo500 force-pushed the feat/sort-field-definitions branch from ded8f68 to c281090 Compare August 10, 2026 06:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Suppressed comments (1)

music_assistant/controllers/music/sorting.py:178

  • [PROBLEM] MEDIA_TYPE_SORT_FIELDS does not include MediaType.PODCAST or MediaType.GENRE, but there are controllers for both (media/podcasts.py, media/genres.py) that now accept the new sorting parameters and expose get_sort_options; as-is, get_sort_options_for_media_type will return an empty list for those media types.
    MediaType.AUDIOBOOK: [
        SortField.NAME,
        SortField.SORT_NAME,
        SortField.TIMESTAMP_ADDED,
        SortField.TIMESTAMP_MODIFIED,

Comment thread music_assistant/controllers/music/media/tracks.py Outdated
Comment thread music_assistant/controllers/music/sorting.py
Copilot AI review requested due to automatic review settings August 10, 2026 06:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

music_assistant/controllers/music/sorting.py:190

  • [PROBLEM] get_sort_options_for_media_type returns an empty list for media types not present in MEDIA_TYPE_SORT_FIELDS (notably PODCAST and GENRE), so the new get_sort_options API will expose no sort UI options for those endpoints despite them supporting order_by/sort_field; add explicit entries for these media types (limited to fields their tables actually support).
    MediaType.PLAYLIST: [
        SortField.NAME,
        SortField.SORT_NAME,
        SortField.TIMESTAMP_ADDED,
        SortField.TIMESTAMP_MODIFIED,
        SortField.LAST_PLAYED,
        SortField.PLAY_COUNT,
        SortField.RANDOM,
        SortField.RANDOM_PLAY_COUNT,
    ],

music_assistant/controllers/music/media/base.py:528

  • [PROBLEM] When sort_field is provided but sort_direction is omitted, this hardcodes an :asc direction, which can contradict the per-field defaults exposed via get_sort_options (e.g., TIMESTAMP_ADDED defaults to DESC) and silently produce unexpected ordering; derive the default direction from the sort option metadata (or leave direction unset) instead of forcing ASC (and apply the same logic in controllers that duplicate this block).
        # Resolve sort parameters: prefer typed parameters over legacy order_by string
        if sort_field is not None:
            # Use new typed parameters
            final_order_by = (
                f"{sort_field.value}:{sort_direction.value if sort_direction else 'asc'}"
            )

Comment thread music_assistant/controllers/music/media/base.py Outdated
Copilot AI review requested due to automatic review settings August 10, 2026 07:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Suppressed comments (3)

music_assistant/controllers/music/sorting.py:187

  • [CRITICAL] MEDIA_TYPE_SORT_FIELDS omits MediaType.PODCAST and MediaType.GENRE, so music/podcasts/get_sort_options and music/genres/get_sort_options will return an empty list even though those controllers support sorting (and now accept sort_field/sort_direction). Add sort-field lists for these media types so clients can build correct sorting UI.
    MediaType.PLAYLIST: [
        SortField.NAME,
        SortField.SORT_NAME,
        SortField.TIMESTAMP_ADDED,
        SortField.TIMESTAMP_MODIFIED,

music_assistant/controllers/music/media/base.py:1813

  • [CRITICAL] _parse_order_by currently accepts any SortField value (including via the new field:direction format) without validating whether it’s supported for the current media type, which can surface as SQL errors (e.g., ordering tracks by year or artists by duration). Since this PR adds a per-media-type sort-options registry, consider rejecting unsupported fields early (return None + warn) based on get_sort_options_for_media_type(self.media_type).
            field_str, direction_str = order_by.split(":", 1)
            try:
                field = SortField(field_str)
                direction = SortDirection(direction_str.lower())
                return (field, direction)

music_assistant/controllers/music/media/base.py:1799

  • [PROBLEM] The new sorting support (typed sort_field/sort_direction params and the field:direction order_by format) isn’t covered by tests: the existing suite only exercises legacy keys like name_desc/sort_name. Adding a few assertions for order_by="name:desc" and sort_field=SortField.NAME would protect the new parsing logic and backward compatibility.
        # Parse new format (e.g., "name:desc" or "name:asc")
        if ":" in order_by:
            field_str, direction_str = order_by.split(":", 1)
            try:
                field = SortField(field_str)
                direction = SortDirection(direction_str.lower())
                return (field, direction)

Copilot AI review requested due to automatic review settings August 10, 2026 07:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Suppressed comments (9)

music_assistant/controllers/music/sorting.py:186

  • [PROBLEM] MEDIA_TYPE_SORT_FIELDS doesn’t include MediaType.PODCAST or MediaType.GENRE, so music/<type>/get_sort_options will return an empty list for those controllers and clients can’t discover supported sort fields/directions for podcasts/genres.
    MediaType.PLAYLIST: [
        SortField.NAME,
        SortField.SORT_NAME,
        SortField.TIMESTAMP_ADDED,
        SortField.TIMESTAMP_MODIFIED,
        SortField.LAST_PLAYED,
        SortField.PLAY_COUNT,
        SortField.RANDOM,
        SortField.RANDOM_PLAY_COUNT,
    ],

music_assistant/controllers/music/media/tracks.py:276

  • [PROBLEM] When callers use the new typed sort_field without sort_direction, this forces :asc for every field; this is inconsistent with the per-field defaults returned by get_sort_options (e.g., TIMESTAMP_ADDED defaults to DESC). Consider centralizing sort-parameter resolution (e.g., a base helper) so all controllers apply the same default-direction logic.
        # Resolve sort parameters: prefer typed parameters over legacy order_by string
        if sort_field is not None:
            final_order_by = (
                f"{sort_field.value}:{sort_direction.value if sort_direction else 'asc'}"
            )

music_assistant/controllers/music/media/base.py:528

  • [PROBLEM] When callers use the new typed sort_field without sort_direction, this forces :asc for every field; that contradicts the per-field defaults advertised by get_sort_options (e.g., TIMESTAMP_ADDED defaults to DESC) and can produce unexpected ordering.
        # Resolve sort parameters: prefer typed parameters over legacy order_by string
        if sort_field is not None:
            # Use new typed parameters
            final_order_by = (
                f"{sort_field.value}:{sort_direction.value if sort_direction else 'asc'}"
            )

music_assistant/controllers/music/media/albums.py:173

  • [PROBLEM] When callers use the new typed sort_field without sort_direction, this forces :asc for every field; this is inconsistent with the per-field defaults returned by get_sort_options (e.g., TIMESTAMP_ADDED defaults to DESC). Consider centralizing sort-parameter resolution so albums/tracks/etc all share the same behavior.
        # Resolve sort parameters: prefer typed parameters over legacy order_by string
        if sort_field is not None:
            final_order_by = (
                f"{sort_field.value}:{sort_direction.value if sort_direction else 'asc'}"
            )

music_assistant/controllers/music/media/artists.py:185

  • [PROBLEM] When callers use the new typed sort_field without sort_direction, this forces :asc for every field; this is inconsistent with the per-field defaults returned by get_sort_options (e.g., LAST_PLAYED defaults to DESC).
        if sort_field is not None:
            final_order_by = (
                f"{sort_field.value}:{sort_direction.value if sort_direction else 'asc'}"
            )

music_assistant/controllers/music/media/audiobooks.py:244

  • [PROBLEM] When callers use the new typed sort_field without sort_direction, this forces :asc for every field; this is inconsistent with the per-field defaults returned by get_sort_options (e.g., TIMESTAMP_ADDED defaults to DESC).
        if sort_field is not None:
            final_order_by = (
                f"{sort_field.value}:{sort_direction.value if sort_direction else 'asc'}"
            )

music_assistant/controllers/music/media/podcasts.py:117

  • [PROBLEM] When callers use the new typed sort_field without sort_direction, this forces :asc for every field; this is inconsistent with the per-field defaults returned by get_sort_options.
        if sort_field is not None:
            final_order_by = (
                f"{sort_field.value}:{sort_direction.value if sort_direction else 'asc'}"
            )

music_assistant/controllers/music/media/genres.py:348

  • [PROBLEM] When callers use the new typed sort_field without sort_direction, this forces :asc for every field; this is inconsistent with the per-field defaults returned by get_sort_options (and would likely be surfaced in the UI as a confusing default sort direction).
        if sort_field is not None:
            final_order_by = (
                f"{sort_field.value}:{sort_direction.value if sort_direction else 'asc'}"
            )

music_assistant/controllers/music/media/base.py:1803

  • [PROBLEM] New sort parsing behavior (field:direction, special handling for RANDOM/*, legacy fallback) isn’t covered by tests; existing tests appear to only exercise legacy order_by values (e.g. name_desc). Adding focused unit/integration coverage for _parse_order_by and ORDER BY generation will prevent regressions during the models/frontend rollout.
    def _parse_order_by(
        self, order_by: str | None
    ) -> tuple[SortField, SortDirection | None] | None:
        """
        Parse order_by string into SortField and SortDirection.

        Supports both new format (field:direction) and legacy format (field_desc).
        Returns None if order_by is None or invalid.
        """
        if not order_by:
            return None

        # Check legacy format first (e.g., "name_desc")
        if order_by in LEGACY_SORT_KEYS:
            return LEGACY_SORT_KEYS[order_by]

        # Parse new format (e.g., "name:desc" or "name:asc")
        if ":" in order_by:
            field_str, direction_str = order_by.split(":", 1)
            try:
                field = SortField(field_str)
                direction = SortDirection(direction_str.lower())
                return (field, direction)
            except ValueError:
                self.logger.warning("Invalid sort field or direction: %s", order_by)
                return None

Copilot AI review requested due to automatic review settings August 10, 2026 07:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Suppressed comments (2)

music_assistant/controllers/music/media/base.py:1824

  • [PROBLEM] _resolve_sort_parameters docstring says it always returns an order_by string in field:direction format, but the function can return legacy values like sort_name (when using order_by/default). This mismatch can mislead future callers/maintainers about what formats _parse_order_by must accept.
        """
        Resolve sort parameters to final order_by string.

        Applies proper defaults: if sort_field is given without sort_direction,
        uses the field's default direction from SORT_FIELD_DEFINITIONS.

        :param sort_field: Optional SortField enum value.
        :param sort_direction: Optional SortDirection enum value.
        :param order_by: Legacy string-based order_by parameter.
        :param default: Default order_by string if none specified.
        :return: Resolved order_by string in 'field:direction' format.
        """

music_assistant/controllers/music/media/base.py:1804

  • [PROBLEM] New sorting behavior (field:direction parsing and typed sort_field/sort_direction) is introduced in _parse_order_by/_get_sort_sql, but there are no tests verifying the new format maps to the expected ORDER BY SQL (including media-specific ARTIST_NAME handling). The repo already has query-builder regression tests (e.g. tests/controllers/music/test_library_listing_queries.py), so adding a few cases there would prevent silent regressions.
    def _parse_order_by(
        self, order_by: str | None
    ) -> tuple[SortField, SortDirection | None] | None:
        """
        Parse order_by string into SortField and SortDirection.

        Supports both new format (field:direction) and legacy format (field_desc).
        Returns None if order_by is None or invalid.
        """
        if not order_by:
            return None

        # Check legacy format first (e.g., "name_desc")
        if order_by in LEGACY_SORT_KEYS:
            return LEGACY_SORT_KEYS[order_by]

        # Parse new format (e.g., "name:desc" or "name:asc")
        if ":" in order_by:
            field_str, direction_str = order_by.split(":", 1)
            try:
                field = SortField(field_str)
                direction = SortDirection(direction_str.lower())
                return (field, direction)
            except ValueError:
                self.logger.warning("Invalid sort field or direction: %s", order_by)
                return None

        # Try parsing as field without direction (default ASC)
        try:
            field = SortField(order_by)
            # Special fields like RANDOM don't support direction
            if field in (SortField.RANDOM, SortField.RANDOM_PLAY_COUNT):
                return (field, None)
            return (field, SortDirection.ASC)
        except ValueError:
            self.logger.warning("Invalid sort field: %s", order_by)
            return None

Comment thread music_assistant/controllers/music/media/albums.py
Comment thread music_assistant/controllers/music/media/albums.py Outdated
Comment thread music_assistant/controllers/music/media/base.py Outdated
Copilot AI review requested due to automatic review settings August 11, 2026 03:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Comment thread music_assistant/controllers/music/media/base.py
Copilot AI review requested due to automatic review settings August 11, 2026 03:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (3)

music_assistant/controllers/music/media/base.py:2275

  • [CRITICAL] _adapt_query_for_collections orders the outer derived query using _get_sort_sql, which qualifies columns as {self.db_table}.<col>; the derived query has no {self.db_table} alias, so this can produce invalid SQL (e.g., ORDER BY albums.search_name). Build the ORDER BY using unqualified derived-column names instead.
            if sql_sort := self._get_sort_sql(field, direction):
                sql_query += f" ORDER BY {sql_sort}"

music_assistant/controllers/music/media/base.py:1808

  • [PROBLEM] _get_sort_sql returns "RANDOM(), play_count" for RANDOM_PLAY_COUNT without qualifying play_count; when caller-provided JOINs include another table with play_count (e.g., tracks joins artists), SQLite can error with an ambiguous column name. Qualify play_count with the controller’s base table for this special case.
        # Special fields like RANDOM don't use direction
        if field in (SortField.RANDOM, SortField.RANDOM_PLAY_COUNT):
            return sql_field

music_assistant/controllers/music/media/base.py:494

  • [PROBLEM] final_order_by is computed and passed into get_library_items_by_query, but the localized-search fallback path (used for Genre/Playlist searches) still forwards the legacy order_by instead of final_order_by, so requests using sort_field/sort_direction can return fallback results in a different sort order.
            offset=offset,
            order_by=final_order_by,
            provider_filter=self._ensure_provider_filter(provider),
            genre_ids=genre,
            played_only=played_only,

@OzGav OzGav added this to the 2.11.0 milestone Aug 11, 2026
Moos, Daniel added 8 commits August 12, 2026 06:30
Per Marcel's review feedback:
- Add sort_field and sort_direction typed parameters to all library_items()
- Deprecate order_by string parameter (kept for backward compatibility)
- Move sort metadata from models to server (sorting.py)
- Per-type SQL overrides in _get_sort_sql() for ARTIST_NAME
- Covers all media types: tracks, albums, artists, podcasts, audiobooks, genres
- Radio and playlists use base.py implementation directly

Related: music-assistant/models#356
- Use final_order_by for ARTIST_NAME JOIN detection (tracks.py)
- Add to_dict() method to SortOptionInfo for JSON serialization
Normalize new field:direction format to legacy field_desc format before
validation against supported_order_keys to prevent fallback when using
typed sort parameters.
Replace string-based validation with SortField enum set:
- supported_order_keys (strings) → supported_sort_fields (SortField set)
- Direct use of _parse_order_by() and _get_sort_sql()
- Unified parsing for both legacy and new formats
…ections

- Add MediaType.PODCAST and MediaType.GENRE to MEDIA_TYPE_SORT_FIELDS
- Add get_default_direction() helper to return per-field defaults
- Add _resolve_sort_parameters() base method for centralized resolution
- Replace duplicated logic in all controllers with helper method
- Ensures typed sort_field without sort_direction uses field's default
  (e.g., TIMESTAMP_ADDED defaults to DESC, not forced ASC)
…reorder library_items parameters, clean up docstrings
Fix Ruff PLC0415 error by moving the import from inside the function
to the module-level imports.
Copilot AI review requested due to automatic review settings August 12, 2026 04:41
@dmoo500
dmoo500 force-pushed the feat/sort-field-definitions branch from fac4f4e to 1f2b473 Compare August 12, 2026 04:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (3)

music_assistant/controllers/music/media/base.py:2311

  • [CRITICAL] _adapt_query_for_collections builds an ORDER BY via _get_sort_sql(), which qualifies columns with the base table name (e.g. albums.search_sort_name), but this query orders a derived UNION/CTE result set where those table names are not in scope, causing SQLite errors like no such column: albums.search_sort_name when collections collapsing is used.
            if sql_sort := self._get_sort_sql(field, direction):
                sql_query += f" ORDER BY {sql_sort}"

music_assistant/controllers/music/media/tracks.py:907

  • [PROBLEM] library_items adds a JOIN to track_artists/artists for ARTIST_NAME sorting and the base query then applies GROUP BY tracks.item_id; ordering by artists.search_name in this grouped query is undefined for multi-artist tracks (SQLite will pick an arbitrary row), leading to unstable/non-deterministic sorting.
        if field == SortField.ARTIST_NAME:
            if direction == SortDirection.DESC:
                return "artists.search_name DESC, tracks.search_name ASC"
            return "artists.search_name ASC, tracks.search_name ASC"

music_assistant/controllers/music/media/albums.py:738

  • [PROBLEM] library_items adds a JOIN to album_artists/artists for ARTIST_NAME sorting and the base query then applies GROUP BY albums.item_id; ordering by artists.search_name in this grouped query is undefined for multi-artist albums (SQLite will pick an arbitrary row), leading to unstable/non-deterministic sorting.
        if field == SortField.ARTIST_NAME:
            if direction == SortDirection.DESC:
                return "artists.search_name DESC, year DESC"
            return "artists.search_name ASC, year DESC"

When collapse_collections=True, the query is wrapped in a CTE where column
names are not table-qualified. Strip table qualifiers from sort SQL to fix
'no such column: audiobooks.search_name' errors.
Copilot AI review requested due to automatic review settings August 12, 2026 04:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (2)

music_assistant/controllers/music/media/base.py:1836

  • [PROBLEM] _get_sort_sql returns the raw BASE_SORT_FIELD_SQL for SortField.RANDOM_PLAY_COUNT without qualifying play_count, which becomes an SQLite "ambiguous column name: play_count" error when the query adds JOINs (e.g., track/album listings joined to artists, which also has a play_count column).
        # Special fields like RANDOM don't use direction
        if field in (SortField.RANDOM, SortField.RANDOM_PLAY_COUNT):
            return sql_field

music_assistant/controllers/music/media/base.py:164

  • [PROBLEM] This PR adds a new API command (music/<type>/get_sort_options), but API_SCHEMA_VERSION is not bumped, so clients cannot reliably feature-detect this endpoint via schema_version. Please bump API_SCHEMA_VERSION in music_assistant/constants.py as required when adding API commands.
        self.mass.register_api_command(
            f"music/{api_base}/get_sort_options",
            self.get_sort_options,
            required_scope=Scope.LIBRARY_READ,
        )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants