diff --git a/server/controllers/albums.controller.ts b/server/controllers/albums.controller.ts
index 6774d767c..fb5ba8da1 100644
--- a/server/controllers/albums.controller.ts
+++ b/server/controllers/albums.controller.ts
@@ -26,13 +26,12 @@ const getList = async (
const { serverId } = req.params;
const { take, skip, serverUrlId, advancedFilters } = req.query;
- const decodedAdvancedFilters = advancedFilters && decodeURI(advancedFilters);
- const jsonAdvancedFilters =
- decodedAdvancedFilters && JSON.parse(decodedAdvancedFilters);
+ const decodedAdvancedFilters =
+ advancedFilters && JSON.parse(decodeURI(advancedFilters));
const albums = await service.albums.findMany({
...req.query,
- advancedFilters: jsonAdvancedFilters,
+ advancedFilters: decodedAdvancedFilters,
serverId,
skip: Number(skip),
take: Number(take),
diff --git a/server/helpers/albums.helpers.ts b/server/helpers/albums.helpers.ts
index 1d086d351..8d5bff8ab 100644
--- a/server/helpers/albums.helpers.ts
+++ b/server/helpers/albums.helpers.ts
@@ -134,8 +134,11 @@ const advancedFilterGroup = (
for (const rule of group.rules) {
if (rule.field && rule.operator) {
const [table, field, relationField] = rule.field.split('.');
- const condition = rule.operator === '!~' ? 'none' : 'some';
+ const condition =
+ rule.operator === '!~' || rule.operator === '!=' ? 'none' : 'some';
const op = operatorMap[rule.operator as keyof typeof operatorMap];
+ const value =
+ field !== 'releaseDate' ? rule.value : new Date(rule.value);
switch (table) {
case 'albums':
@@ -144,7 +147,7 @@ const advancedFilterGroup = (
[field]: {
[condition]: {
[relationField]: {
- [op]: rule.value,
+ [op]: value,
},
userId: user.id,
},
@@ -152,13 +155,24 @@ const advancedFilterGroup = (
});
break;
}
-
+ if (field === 'genres') {
+ query[rootType].push({
+ [field]: {
+ [condition]: {
+ [relationField]: {
+ equals: value,
+ },
+ },
+ },
+ });
+ break;
+ }
query[rootType].push({
[field]: {
mode: insensitiveFields.includes(field)
? 'insensitive'
: undefined,
- [op]: rule.value,
+ [op]: value,
},
});
break;
@@ -171,7 +185,7 @@ const advancedFilterGroup = (
[field]: {
some: {
[relationField]: {
- [op]: rule.value,
+ [op]: value,
},
userId: user.id,
},
@@ -181,13 +195,29 @@ const advancedFilterGroup = (
});
break;
}
+ if (field === 'genres') {
+ query[rootType].push({
+ [table]: {
+ some: {
+ [field]: {
+ [condition]: {
+ [relationField]: {
+ equals: value,
+ },
+ },
+ },
+ },
+ },
+ });
+ break;
+ }
query[rootType].push({
[table]: {
[condition]: {
[field]: {
mode: 'insensitive',
- [op]: rule.value,
+ [op]: value,
},
},
},
@@ -218,8 +248,10 @@ const advancedFilter = (filter: AdvancedFilterGroup, user: AuthUser) => {
for (const rule of filter.rules) {
if (rule.field && rule.operator) {
let [table, field, relationField] = rule.field.split('.');
- const condition = rule.operator === '!~' ? 'none' : 'some';
+ const condition =
+ rule.operator === '!~' || rule.operator === '!=' ? 'none' : 'some';
const op = operatorMap[rule.operator as keyof typeof operatorMap];
+ const value = field !== 'releaseDate' ? rule.value : new Date(rule.value);
switch (table) {
case 'albums':
@@ -228,7 +260,7 @@ const advancedFilter = (filter: AdvancedFilterGroup, user: AuthUser) => {
[field]: {
[condition]: {
[relationField]: {
- [op]: rule.value,
+ [op]: value,
},
userId: user.id,
},
@@ -236,13 +268,24 @@ const advancedFilter = (filter: AdvancedFilterGroup, user: AuthUser) => {
});
break;
}
-
+ if (field === 'genres') {
+ rootQuery[rootQueryType].push({
+ [field]: {
+ [condition]: {
+ [relationField]: {
+ equals: value,
+ },
+ },
+ },
+ });
+ break;
+ }
rootQuery[rootQueryType].push({
[field]: {
mode: insensitiveFields.includes(field)
? 'insensitive'
: undefined,
- [op]: rule.value,
+ [op]: value,
},
});
break;
@@ -255,7 +298,7 @@ const advancedFilter = (filter: AdvancedFilterGroup, user: AuthUser) => {
[field]: {
some: {
[relationField]: {
- [op]: rule.value,
+ [op]: value,
},
userId: user.id,
},
@@ -265,13 +308,29 @@ const advancedFilter = (filter: AdvancedFilterGroup, user: AuthUser) => {
});
break;
}
+ if (field === 'genres') {
+ rootQuery[rootQueryType].push({
+ [table]: {
+ some: {
+ [field]: {
+ [condition]: {
+ [relationField]: {
+ equals: value,
+ },
+ },
+ },
+ },
+ },
+ });
+ break;
+ }
rootQuery[rootQueryType].push({
[table]: {
[condition]: {
[field]: {
mode: 'insensitive',
- [op]: rule.value,
+ [op]: value,
},
},
},
diff --git a/src/renderer/features/albums/components/advanced-filters.tsx b/src/renderer/features/albums/components/advanced-filters.tsx
index 6500629a7..8cbc5296b 100644
--- a/src/renderer/features/albums/components/advanced-filters.tsx
+++ b/src/renderer/features/albums/components/advanced-filters.tsx
@@ -1,5 +1,7 @@
+import { useMemo } from 'react';
import { Stack, Group } from '@mantine/core';
import dayjs from 'dayjs';
+import { AnimatePresence, motion } from 'framer-motion';
import get from 'lodash/get';
import set from 'lodash/set';
import { nanoid } from 'nanoid/non-secure';
@@ -12,6 +14,7 @@ import {
Select,
TextInput,
} from '@/renderer/components';
+import { useGenreList } from '@/renderer/features/genres';
export enum FilterGroupType {
AND = 'AND',
@@ -19,10 +22,10 @@ export enum FilterGroupType {
}
export type AdvancedFilterRule = {
- field: string | null;
- operator: string | null;
+ field?: string | null;
+ operator?: string | null;
uniqueId: string;
- value: string | number | Date | undefined | null | any;
+ value?: string | number | Date | undefined | null | any;
};
export type AdvancedFilterGroup = {
@@ -58,8 +61,8 @@ const NUMBER_FILTER_OPTIONS_DATA = [
];
const ID_FILTER_OPTIONS_DATA = [
- { label: 'is', value: 'equals' },
- { label: 'is not', value: 'not' },
+ { label: 'is', value: '=' },
+ { label: 'is not', value: '!=' },
];
const FILTER_GROUP_OPTIONS_DATA = [
@@ -75,73 +78,96 @@ const FILTER_GROUP_OPTIONS_DATA = [
const FILTER_OPTIONS_DATA = [
{
+ default: '~',
label: 'Artist Title',
value: 'artists.name',
},
{
+ default: '=',
label: 'Artist Rating',
value: 'artists.ratings.value',
},
{
+ default: '=',
label: 'Artist Genre',
- value: 'artists.genre',
+ value: 'artists.genres.id',
},
{
+ default: '~',
label: 'Album Artist Title',
value: 'albumArtists.name',
},
{
+ default: '=',
label: 'Album Artist Rating',
value: 'albumArtists.ratings.value',
},
{
+ default: '=',
label: 'Album Artist Genre',
- value: 'albumArtists.genre',
+ value: 'albumArtists.genres.id',
},
{
+ default: '~',
label: 'Album Title',
value: 'albums.name',
},
{
- label: 'Album Genre',
- value: 'albums.genre',
- },
- {
+ default: '=',
label: 'Album Rating',
value: 'albums.ratings.value',
},
{
+ default: '=',
+ label: 'Album Genre',
+ value: 'albums.genres.id',
+ },
+ {
+ default: '=',
label: 'Album Year',
value: 'albums.releaseYear',
},
{
+ default: '<',
label: 'Album Release Date',
value: 'albums.releaseDate',
},
{
- label: 'Album Plays',
+ default: '=',
+ disabled: true,
+ label: 'Album Play Count',
value: 'albums.playCount',
},
{
+ default: '<',
label: 'Album Date Added',
value: 'albums.dateAdded',
},
{
+ default: '~',
label: 'Track Title',
value: 'songs.name',
},
{
- label: 'Track Plays',
- value: 'songs.playCount',
- },
- {
+ default: '=',
label: 'Track Rating',
value: 'songs.ratings.value',
},
+ {
+ default: '=',
+ label: 'Track Genre',
+ value: 'songs.genres.id',
+ },
+ {
+ default: '=',
+ disabled: true,
+ label: 'Track Play Count',
+ value: 'songs.playCount',
+ },
];
const OPTIONS_MAP = {
- 'albumArtists.genre': {
+ 'albumArtists.genres.id': {
type: 'id',
},
'albumArtists.name': {
@@ -156,7 +182,7 @@ const OPTIONS_MAP = {
'albums.favorite': {
type: 'boolean',
},
- 'albums.genre': {
+ 'albums.genres.id': {
type: 'id',
},
'albums.name': {
@@ -174,7 +200,7 @@ const OPTIONS_MAP = {
'albums.releaseYear': {
type: 'number',
},
- 'artists.genre': {
+ 'artists.genres.id': {
type: 'id',
},
'artists.name': {
@@ -183,6 +209,9 @@ const OPTIONS_MAP = {
'artists.ratings.value': {
type: 'number',
},
+ 'songs.genres.id': {
+ type: 'id',
+ },
'songs.name': {
type: 'string',
},
@@ -218,7 +247,7 @@ export const formatAdvancedFiltersGroups = (groups: AdvancedFilterGroup[]) => {
};
// Prevent query key from constantly changing due to empty rules or groups
-export const formatAdvancedFiltersQuery = (filter: AdvancedFilterGroup) => {
+export const encodeAdvancedFiltersQuery = (filter: AdvancedFilterGroup) => {
const updatedFilter = {
...filter,
group: formatAdvancedFiltersGroups(filter.group),
@@ -227,7 +256,7 @@ export const formatAdvancedFiltersQuery = (filter: AdvancedFilterGroup) => {
.map((rule) => ({ ...rule, uniqueId: undefined })),
};
- return updatedFilter;
+ return encodeURI(JSON.stringify(updatedFilter));
};
interface FilterOptionProps {
@@ -252,6 +281,48 @@ const FilterOption = ({
onChangeValue,
}: FilterOptionProps) => {
const { field, operator, uniqueId, value } = data;
+ const { data: genres } = useGenreList();
+
+ const genresData = useMemo(() => {
+ if (!genres?.data) return null;
+
+ const album = [];
+ const song = [];
+ const albumArtist = [];
+ const artist = [];
+
+ for (const genre of genres.data) {
+ if (genre.albumCount > 0) {
+ album.push({
+ label: `${genre.name} (${genre.albumCount})`,
+ value: genre.id,
+ });
+ }
+
+ if (genre.songCount > 0) {
+ song.push({
+ label: `${genre.name} (${genre.songCount})`,
+ value: genre.id,
+ });
+ }
+
+ if (genre.albumArtistCount > 0) {
+ albumArtist.push({
+ label: `${genre.name} (${genre.albumArtistCount})`,
+ value: genre.id,
+ });
+ }
+
+ if (genre.artistCount > 0) {
+ artist.push({
+ label: `${genre.name} (${genre.artistCount})`,
+ value: genre.id,
+ });
+ }
+ }
+
+ return { album, albumArtist, artist, song };
+ }, [genres]);
const handleDeleteRule = () => {
onDeleteRule({ groupIndex, level, uniqueId });
@@ -281,6 +352,17 @@ const FilterOption = ({
});
}
+ const isDate = e instanceof Date;
+
+ if (isDate) {
+ return onChangeValue({
+ groupIndex,
+ level,
+ uniqueId,
+ value: dayjs(e).format('YYYY-MM-DD'),
+ });
+ }
+
return onChangeValue({
groupIndex,
level,
@@ -337,10 +419,10 @@ const FilterOption = ({
};
const filterInputValueMap = {
- 'albumArtists.genre': (
+ 'albumArtists.genres.id': (
),
- 'albums.genre': (
+ 'albums.genres.id': (
),
- 'artists.genre': (
+ 'artists.genres.id': (
),
+ 'songs.genres.id': (
+
+ ),
'songs.name': (
- {data.rules.map((rule: AdvancedFilterRule) => (
-
- ))}
- {data.group && (
- <>
- {data.group.map((group: AdvancedFilterGroup, index: number) => (
-
+ {data.rules.map((rule: AdvancedFilterRule) => (
+
+
+
+ ))}
+
+ {data.group && (
+
+ {data.group.map((group: AdvancedFilterGroup, index: number) => (
+
+
+
))}
- >
+
)}
);
@@ -701,10 +810,10 @@ export const AdvancedFilters = ({ filters, setFilters }: any) => {
group: [],
rules: [
{
- field: undefined,
- operator: undefined,
+ field: '',
+ operator: '',
uniqueId: nanoid(),
- value: undefined,
+ value: '',
},
],
type: FilterGroupType.AND,
@@ -800,11 +909,15 @@ export const AdvancedFilters = ({ filters, setFilters }: any) => {
path,
get(filtersCopy, path).map((rule: AdvancedFilterRule) => {
if (rule.uniqueId !== uniqueId) return rule;
+ const defaultOperator = FILTER_OPTIONS_DATA.find(
+ (option) => option.value === value
+ )?.default;
+
return {
...rule,
field: value,
- operator: null,
- value: null,
+ operator: defaultOperator || '',
+ value: '',
};
})
);