From dcb27e2a4fb39f3eefc92bbc9e3c7cc2fc17b4d2 Mon Sep 17 00:00:00 2001 From: Jozef Kruszynski Date: Sat, 7 Mar 2026 23:43:39 +0100 Subject: [PATCH 1/2] feat(genres): add genre exclusions for media items --- src/components/InfoHeader.vue | 35 +++++ src/components/ProviderDetails.vue | 9 ++ .../genre/GenreExclusionManager.vue | 139 ++++++++++++++++++ src/plugins/api/index.ts | 34 +++++ src/plugins/eventbus.ts | 1 + src/translations/en.json | 3 + 6 files changed, 221 insertions(+) create mode 100644 src/components/genre/GenreExclusionManager.vue diff --git a/src/components/InfoHeader.vue b/src/components/InfoHeader.vue index e78a204c9..7fd6c9c6a 100644 --- a/src/components/InfoHeader.vue +++ b/src/components/InfoHeader.vue @@ -377,6 +377,9 @@ outlined class="cursor-pointer" @click="handleMediaItemClick(genre, 0, 0)" + @contextmenu.prevent=" + (e: MouseEvent) => showGenreChipContextMenu(e, genre) + " > {{ getGenreDisplayName(genre.name, genre.translation_key, t, te) @@ -506,6 +509,38 @@ watch( { immediate: true }, ); +const showGenreChipContextMenu = (evt: MouseEvent, genre: Genre) => { + if ( + !compProps.item || + !isAdmin.value || + compProps.item.provider !== "library" + ) + return; + const mediaItem = compProps.item; + const menuItems: ContextMenuItem[] = [ + { + label: "exclude_genre", + icon: "mdi-cancel", + action: async () => { + await api.excludeGenreFromItem( + genre.item_id, + mediaItem.media_type, + mediaItem.item_id, + ); + mappedGenres.value = mappedGenres.value.filter( + (g) => g.item_id !== genre.item_id, + ); + eventbus.emit("genreExcluded"); + }, + }, + ]; + eventbus.emit("contextmenu", { + items: menuItems, + posX: evt.clientX, + posY: evt.clientY, + }); +}; + const albumClick = function (item: Album | ItemMapping) { // album entry clicked router.push({ diff --git a/src/components/ProviderDetails.vue b/src/components/ProviderDetails.vue index e6a8b5eff..9f119d7d4 100644 --- a/src/components/ProviderDetails.vue +++ b/src/components/ProviderDetails.vue @@ -125,10 +125,19 @@ + diff --git a/src/plugins/api/index.ts b/src/plugins/api/index.ts index ee45791bb..206c92858 100644 --- a/src/plugins/api/index.ts +++ b/src/plugins/api/index.ts @@ -1033,6 +1033,40 @@ export class MusicAssistantApi { }); } + public excludeGenreFromItem( + genre_id: string, + media_type: string, + media_id: string, + ): Promise { + return this.sendCommand("music/genres/exclude_genre_from_media_item", { + genre_id, + media_type, + media_id, + }); + } + + public removeGenreExclusion( + genre_id: string, + media_type: string, + media_id: string, + ): Promise { + return this.sendCommand("music/genres/remove_genre_exclusion", { + genre_id, + media_type, + media_id, + }); + } + + public getGenreExclusionsForItem( + media_type: string, + media_id: string, + ): Promise { + return this.sendCommand("music/genres/genre_exclusions_for_media_item", { + media_type, + media_id, + }); + } + public async getGenreOverviewRows( item_id: string, provider_instance_id_or_domain: string, diff --git a/src/plugins/eventbus.ts b/src/plugins/eventbus.ts index baaaa8f80..92b5440f7 100644 --- a/src/plugins/eventbus.ts +++ b/src/plugins/eventbus.ts @@ -43,6 +43,7 @@ export type Events = { deleteGenreDialog: DeleteGenreDialogEvent; linkGenreDialog: LinkGenreDialogEvent; clearSelection: void; + genreExcluded: void; "homescreen-edit-toggle": void; "mobile-sidebar-open": void; }; diff --git a/src/translations/en.json b/src/translations/en.json index 353763dbe..e002e1cd7 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -317,6 +317,9 @@ "remove": "Remove", "remove_alias": "Remove alias", "remove_alias_failed": "Failed to remove alias", + "exclude_genre": "Exclude genre", + "genre_exclusions": "Genre Exclusions", + "remove_genre_exclusion": "Remove exclusion", "remove_library": "Remove from library", "remove_playlist": "Remove from playlist", "search": "Search", From e2b5a46f0f5be10626f7a63c2b3c1c54c0658ed0 Mon Sep 17 00:00:00 2001 From: Jozef Kruszynski Date: Tue, 10 Mar 2026 19:27:51 +0100 Subject: [PATCH 2/2] fix(genres): remove unnecessary double translation --- src/components/genre/GenreExclusionManager.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/genre/GenreExclusionManager.vue b/src/components/genre/GenreExclusionManager.vue index 4a8920484..79e29d60b 100644 --- a/src/components/genre/GenreExclusionManager.vue +++ b/src/components/genre/GenreExclusionManager.vue @@ -108,7 +108,7 @@ const onMenu = (evt: Event, genre: Genre) => { eventbus.emit("contextmenu", { items: [ { - label: t("remove_genre_exclusion"), + label: "remove_genre_exclusion", icon: "mdi-delete", action: () => removeExclusion(genre), disabled: operationInProgress.value,