Skip to content

Commit 2186707

Browse files
Merge pull request #1547 from music-assistant/genre-exclusions
Add genre exclusion feature to UI
2 parents 0bf19ce + e2b5a46 commit 2186707

File tree

6 files changed

+221
-0
lines changed

6 files changed

+221
-0
lines changed

src/components/InfoHeader.vue

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,9 @@
377377
outlined
378378
class="cursor-pointer"
379379
@click="handleMediaItemClick(genre, 0, 0)"
380+
@contextmenu.prevent="
381+
(e: MouseEvent) => showGenreChipContextMenu(e, genre)
382+
"
380383
>
381384
{{
382385
getGenreDisplayName(genre.name, genre.translation_key, t, te)
@@ -506,6 +509,38 @@ watch(
506509
{ immediate: true },
507510
);
508511
512+
const showGenreChipContextMenu = (evt: MouseEvent, genre: Genre) => {
513+
if (
514+
!compProps.item ||
515+
!isAdmin.value ||
516+
compProps.item.provider !== "library"
517+
)
518+
return;
519+
const mediaItem = compProps.item;
520+
const menuItems: ContextMenuItem[] = [
521+
{
522+
label: "exclude_genre",
523+
icon: "mdi-cancel",
524+
action: async () => {
525+
await api.excludeGenreFromItem(
526+
genre.item_id,
527+
mediaItem.media_type,
528+
mediaItem.item_id,
529+
);
530+
mappedGenres.value = mappedGenres.value.filter(
531+
(g) => g.item_id !== genre.item_id,
532+
);
533+
eventbus.emit("genreExcluded");
534+
},
535+
},
536+
];
537+
eventbus.emit("contextmenu", {
538+
items: menuItems,
539+
posX: evt.clientX,
540+
posY: evt.clientY,
541+
});
542+
};
543+
509544
const albumClick = function (item: Album | ItemMapping) {
510545
// album entry clicked
511546
router.push({

src/components/ProviderDetails.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,19 @@
125125
</v-list>
126126
</Container>
127127
</section>
128+
<GenreExclusionManager
129+
v-if="
130+
itemDetails.provider === 'library' &&
131+
itemDetails.media_type !== MediaType.GENRE
132+
"
133+
:media-type="itemDetails.media_type"
134+
:media-id="itemDetails.item_id"
135+
/>
128136
</template>
129137

130138
<script setup lang="ts">
131139
import Container from "@/components/Container.vue";
140+
import GenreExclusionManager from "@/components/genre/GenreExclusionManager.vue";
132141
import ListItem from "@/components/ListItem.vue";
133142
import ProviderIcon from "@/components/ProviderIcon.vue";
134143
import { iconHiRes } from "@/components/QualityDetailsBtn.vue";
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<template>
2+
<section v-if="isAdmin" style="margin-bottom: 10px">
3+
<Toolbar
4+
:title="exclusionTitle"
5+
:menu-items="toolbarMenuItems"
6+
@title-clicked="toggleSection"
7+
/>
8+
<v-divider />
9+
<Container v-if="sectionExpanded">
10+
<v-list>
11+
<ListItem
12+
v-for="genre in exclusions"
13+
:key="genre.item_id"
14+
show-menu-btn
15+
@menu.stop="(evt) => onMenu(evt, genre)"
16+
>
17+
<template #prepend>
18+
<div
19+
style="
20+
width: 30px;
21+
margin-left: 10px;
22+
margin-right: 10px;
23+
display: flex;
24+
align-items: center;
25+
"
26+
>
27+
<Compass :size="30" />
28+
</div>
29+
</template>
30+
<template #title>{{
31+
getGenreDisplayName(genre.name, genre.translation_key, t, te)
32+
}}</template>
33+
</ListItem>
34+
</v-list>
35+
</Container>
36+
</section>
37+
</template>
38+
39+
<script setup lang="ts">
40+
import Container from "@/components/Container.vue";
41+
import ListItem from "@/components/ListItem.vue";
42+
import Toolbar, { ToolBarMenuItem } from "@/components/Toolbar.vue";
43+
import { getGenreDisplayName } from "@/helpers/utils";
44+
import { api } from "@/plugins/api";
45+
import { Genre, MediaType } from "@/plugins/api/interfaces";
46+
import { authManager } from "@/plugins/auth";
47+
import { eventbus } from "@/plugins/eventbus";
48+
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
49+
import { useI18n } from "vue-i18n";
50+
import { Compass, ChevronUp, ChevronDown } from "lucide-vue-next";
51+
52+
interface Props {
53+
mediaType: MediaType;
54+
mediaId: string;
55+
}
56+
57+
const props = defineProps<Props>();
58+
59+
const { t, te } = useI18n();
60+
61+
const isAdmin = computed(() => authManager.isAdmin());
62+
const sectionExpanded = ref(false);
63+
const operationInProgress = ref(false);
64+
const exclusions = ref<Genre[]>([]);
65+
66+
const exclusionTitle = computed(
67+
() => `${t("genre_exclusions")} (${exclusions.value.length})`,
68+
);
69+
70+
const toolbarMenuItems = computed<ToolBarMenuItem[]>(() => [
71+
{
72+
label: "tooltip.collapse_expand",
73+
icon: sectionExpanded.value ? ChevronUp : ChevronDown,
74+
action: toggleSection,
75+
overflowAllowed: false,
76+
},
77+
]);
78+
79+
const loadExclusions = async () => {
80+
try {
81+
exclusions.value = await api.getGenreExclusionsForItem(
82+
props.mediaType,
83+
props.mediaId,
84+
);
85+
} catch {
86+
exclusions.value = [];
87+
}
88+
};
89+
90+
const removeExclusion = async (genre: Genre) => {
91+
operationInProgress.value = true;
92+
try {
93+
await api.removeGenreExclusion(
94+
genre.item_id,
95+
props.mediaType,
96+
props.mediaId,
97+
);
98+
exclusions.value = exclusions.value.filter(
99+
(g) => g.item_id !== genre.item_id,
100+
);
101+
} finally {
102+
operationInProgress.value = false;
103+
}
104+
};
105+
106+
const onMenu = (evt: Event, genre: Genre) => {
107+
const mouseEvt = evt as MouseEvent;
108+
eventbus.emit("contextmenu", {
109+
items: [
110+
{
111+
label: "remove_genre_exclusion",
112+
icon: "mdi-delete",
113+
action: () => removeExclusion(genre),
114+
disabled: operationInProgress.value,
115+
},
116+
],
117+
posX: mouseEvt.clientX,
118+
posY: mouseEvt.clientY,
119+
});
120+
};
121+
122+
const toggleSection = () => {
123+
sectionExpanded.value = !sectionExpanded.value;
124+
};
125+
126+
onMounted(() => {
127+
loadExclusions();
128+
eventbus.on("genreExcluded", loadExclusions);
129+
});
130+
131+
onBeforeUnmount(() => {
132+
eventbus.off("genreExcluded", loadExclusions);
133+
});
134+
135+
watch(
136+
() => props.mediaId,
137+
() => loadExclusions(),
138+
);
139+
</script>

src/plugins/api/index.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,40 @@ export class MusicAssistantApi {
10331033
});
10341034
}
10351035

1036+
public excludeGenreFromItem(
1037+
genre_id: string,
1038+
media_type: string,
1039+
media_id: string,
1040+
): Promise<void> {
1041+
return this.sendCommand("music/genres/exclude_genre_from_media_item", {
1042+
genre_id,
1043+
media_type,
1044+
media_id,
1045+
});
1046+
}
1047+
1048+
public removeGenreExclusion(
1049+
genre_id: string,
1050+
media_type: string,
1051+
media_id: string,
1052+
): Promise<void> {
1053+
return this.sendCommand("music/genres/remove_genre_exclusion", {
1054+
genre_id,
1055+
media_type,
1056+
media_id,
1057+
});
1058+
}
1059+
1060+
public getGenreExclusionsForItem(
1061+
media_type: string,
1062+
media_id: string,
1063+
): Promise<Genre[]> {
1064+
return this.sendCommand("music/genres/genre_exclusions_for_media_item", {
1065+
media_type,
1066+
media_id,
1067+
});
1068+
}
1069+
10361070
public async getGenreOverviewRows(
10371071
item_id: string,
10381072
provider_instance_id_or_domain: string,

src/plugins/eventbus.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export type Events = {
4343
deleteGenreDialog: DeleteGenreDialogEvent;
4444
linkGenreDialog: LinkGenreDialogEvent;
4545
clearSelection: void;
46+
genreExcluded: void;
4647
"homescreen-edit-toggle": void;
4748
"mobile-sidebar-open": void;
4849
};

src/translations/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,9 @@
317317
"remove": "Remove",
318318
"remove_alias": "Remove alias",
319319
"remove_alias_failed": "Failed to remove alias",
320+
"exclude_genre": "Exclude genre",
321+
"genre_exclusions": "Genre Exclusions",
322+
"remove_genre_exclusion": "Remove exclusion",
320323
"remove_library": "Remove from library",
321324
"remove_playlist": "Remove from playlist",
322325
"search": "Search",

0 commit comments

Comments
 (0)