Skip to content

Feat/genres-v2-implementation#3164

Draft
jozefKruszynski wants to merge 3 commits intodevfrom
feat/genres-v2-implementation
Draft

Feat/genres-v2-implementation#3164
jozefKruszynski wants to merge 3 commits intodevfrom
feat/genres-v2-implementation

Conversation

@jozefKruszynski
Copy link
Contributor

@jozefKruszynski jozefKruszynski commented Feb 14, 2026

Genre V2: Complete Genre System with Aliases

Overview

This PR implements a complete overhaul of the genre system in Music Assistant, replacing the legacy metadata.genres approach with a flexible, user-managed genre system featuring genre aliases, full CRUD operations, and powerful filtering capabilities.

Branch: feat/genres-v2-implementation
Target: dev
Type: Feature


Features Implemented

1. Core Genre System

Genre Management

  • Full CRUD operations for genres (Create, Read, Update, Delete)
  • User-created custom genres with metadata support
  • 100+ pre-configured default genres with restore functionality
  • Genre favorites and search capabilities
  • Translation keys for internationalization support

Alias System

  • Genre aliases for flexible music tagging (e.g., "indie rock", "classic rock" -> "Rock")
  • Many-to-many relationships: genres <-> aliases <-> media items
  • Automatic self-alias creation (genre automatically gets its own name as an alias)
  • Full CRUD operations for aliases
  • Reverse lookup: aliases can show which genres they belong to

Media Integration

  • Genre filtering on tracks, albums, and artists via genre_ids parameter
  • Multi-genre filtering with OR logic (genre_ids=[1,2,3])
  • Transitive relationship: Genre -> Aliases -> Media
  • Provider sync integration for automatic genre tagging

2. New API Endpoints

Genre Endpoints (music/genres/)

Method Endpoint Description
GET /count Get total genre count
GET /library_items Get all genres with filtering/search
GET /get Get single genre by ID
GET /overview Get genre overview (recommendation rows)
GET /radio_mode_base_tracks Get tracks for radio mode
POST /add Create new genre (admin)
PUT /update Update existing genre (admin)
DELETE /remove Delete genre (admin)
POST /add_alias_mapping Map alias to genre (admin)
DELETE /remove_alias_mapping Unmap alias from genre (admin)
POST /restore_defaults Restore default genres (partial or full) (admin)

Alias Endpoints (music/aliases/)

Method Endpoint Description
GET /library_items Get all aliases with filtering/search
GET /get Get single alias by ID
POST /add Create new alias (admin)
PUT /update Update existing alias (admin)
DELETE /remove Delete alias (admin)
POST /add_media_mapping Tag media with alias (admin)
DELETE /remove_media_mapping Remove alias from media (admin)
POST /promote_to_genre Promote alias to parent genre (admin)

Media Filtering (Updated existing endpoints)

All media endpoints now support:

genre_ids: int | list[int]  - Filter by genre(s)

Examples:
  music/tracks/library_items?genre_ids=123
  music/albums/library_items?genre_ids=[123,456]
  music/artists/library_items?genre_ids=123

3. Alias Promotion

Purpose: Allows promoting an alias to become a standalone parent genre when it deserves its own top-level category.

What it does:

  • Creates a new Genre with the alias's name, sort_name, and metadata
  • Remaps the alias to belong to the new genre (as its primary alias)
  • Preserves all media->alias mappings (no media items are affected)
  • Automatically creates a self-alias for the new genre

Example:

Before:
  Genre: "Rock"
    -> Alias: "indie rock" -> [1000 tracks]

After promotion:
  Genre: "Indie Rock" (newly created)
    -> Alias: "indie rock" -> [1000 tracks] (same media preserved)

Protection:

  • Self-aliases cannot be promoted (would create duplicates)
  • All media associations are preserved
  • Requires admin role

4. Self-Alias Protection

What are self-aliases?
Self-aliases are automatically created when a genre is added. They match the genre's name exactly (e.g., "Rock" genre has a "Rock" alias). These are fundamental to the genre system.

Protection Mechanisms:

  1. Cannot delete self-aliases (music/aliases/remove)

    • Raises ValueError with clear guidance
    • Directs users to delete the parent genre instead
    • Non-self-aliases can be deleted normally
  2. Cannot unlink genre from its own self-alias (music/genres/remove_alias_mapping)

    • Raises ValueError explaining self-aliases cannot be unlinked
    • Prevents breaking the genre<->self-alias relationship
    • Unlinking self-aliases from other genres is allowed (edge case)
  3. Cannot promote self-aliases (music/aliases/promote_to_genre)

    • Raises ValueError to prevent creating duplicate genres
    • Non-self-aliases can be promoted normally

Implementation:

# Check if alias name matches parent genre name (case-insensitive)
if genre.name.lower() == alias.name.lower():
    raise ValueError("Cannot [delete/unlink/promote] self-alias...")

5. Genre Overview Endpoint

Purpose: Optimized single-call solution for building genre detail pages.

What it returns:

  • Pre-organized recommendation rows (similar to Netflix/Spotify UI)
  • Up to 7 media type rows: Artists, Albums, Tracks, Playlists, Radio, Podcasts, Audiobooks
  • Empty rows automatically filtered out
  • Browse paths included for "View All" navigation
  • Consistent limits across all media types

Benefits:

  • 7x fewer API calls — single request instead of 7 separate queries
  • Pre-filtered — empty rows automatically removed
  • Pre-organized — RecommendationFolder structure ready for UI
  • Browse paths — "View All" links included
  • Faster page load — single round-trip to backend

Example Usage:

const overview = await api.call("music/genres/overview", {
  item_id: genreId,
  provider_instance_id_or_domain: "library"
});

// Returns array of RecommendationFolder objects ready to render
overview.forEach(row => {
  renderMediaRow(row.label, row.items, row.path);
});

6. Reverse Lookup for Aliases

Feature: GenreAlias objects now include a genres field showing parent genres.

Use Cases:

  • Admin interfaces showing alias -> genre relationships
  • Genre page search showing "indie rock -> Rock"
  • Debugging genre mapping issues
  • Understanding genre structure when managing library

Implementation:

async def _attach_genres(self, aliases: list[GenreAlias]) -> None:
    """Attach parent genres to alias objects by querying mapping tables."""
    # Queries genre_alias_mapping + genres tables
    # Populates alias.genres field

Benefits:

  • Always populated on GenreAlias objects
  • Minimal performance impact (one extra query per alias fetch)
  • Helps users understand and organize their genre structure

7. Restore Default Genres (Dual Mode)

Feature: The music/genres/restore_defaults endpoint supports two restore modes for different use cases.

Partial Restore (Default - Non-destructive):

  • Adds any missing default genres from genre_mapping.json
  • Ensures self-aliases exist for all existing genres
  • Ensures configured aliases exist for all existing genres
  • Does NOT delete or modify existing genres
  • Safe to run multiple times (idempotent)
await api.call("music/genres/restore_defaults", {
    full_restore: False  # or omit (defaults to False)
})

Full Restore (Destructive):

  • Deletes ALL existing genres and aliases
  • Deletes ALL genre→media associations
  • Recreates everything from default mapping
  • Essentially a "reset to factory defaults"
  • Requires explicit user confirmation in UI
await api.call("music/genres/restore_defaults", {
    full_restore: True
})

Use Cases:

  • Partial: First-run setup, recovering deleted defaults, adding new defaults after updates
  • Full: Corrupted genre library, user wants fresh start, testing/development

UI Integration:

  • Partial restore: Single confirmation, shown in welcome flow and settings
  • Full restore: Double confirmation with typed text, hidden behind "Advanced" toggle
  • See Use Case #8 in Frontend Guide

@github-actions
Copy link
Contributor

github-actions bot commented Feb 14, 2026

🔒 Dependency Security Report

📦 Modified Dependencies

The following dependencies were added or modified:

diff --git a/requirements_all.txt b/requirements_all.txt
index 247f2374..a5110069 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -42,7 +42,7 @@ librosa==0.11.0
 lyricsgenius==3.7.5
 mashumaro==3.18
 music-assistant-frontend==2.17.86
-music-assistant-models==1.1.98
+music-assistant-models @ git+https://github.com/music-assistant/models.git@feat/genres-v2-implementation
 mutagen==1.47.0
 niconico.py-ma==2.1.0.post1
 orjson==3.11.5

New/modified packages to review:

  • music-assistant-models @ git+https://github.com/music-assistant/models.git@feat/genres-v2-implementation

🔍 Vulnerability Scan Results

No known vulnerabilities found

Name Skip Reason
music_assistant_models Dependency not found on PyPI and could not be audited: music-assistant-models (0.0.0)
✅ No known vulnerabilities found

Automated Security Checks

  • Vulnerability Scan: Passed - No known vulnerabilities
  • Trusted Sources: All packages have verified source repositories
  • Typosquatting Check: No suspicious package names detected
  • License Compatibility: All licenses are OSI-approved and compatible
  • Supply Chain Risk: Passed - packages appear mature and maintained

Manual Review

Maintainer approval required:

  • I have reviewed the changes above and approve these dependency updates

To approve: Comment /approve-dependencies or manually add the dependencies-reviewed label.

offset: int = 0,
order_by: str = "sort_name",
provider: str | list[str] | None = None,
genre_ids: int | list[int] | None = None,
Copy link
Member

Choose a reason for hiding this comment

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

if we want to be consistent with the provider filter above, we may want to name this just genre ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, I'll also update that this evening

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.

2 participants