diff --git a/model/genres.go b/model/genre.go similarity index 68% rename from model/genres.go rename to model/genre.go index f7da78bcb..50f945ae4 100644 --- a/model/genres.go +++ b/model/genre.go @@ -2,9 +2,9 @@ package model type Genre struct { ID string `json:"id" orm:"column(id)"` - Name string - SongCount int `json:"-"` - AlbumCount int `json:"-"` + Name string `json:"name"` + SongCount int `json:"-"` + AlbumCount int `json:"-"` } type Genres []Genre diff --git a/persistence/album_repository.go b/persistence/album_repository.go index f0b7e16a5..12a147c65 100644 --- a/persistence/album_repository.go +++ b/persistence/album_repository.go @@ -82,7 +82,12 @@ func artistFilter(field string, value interface{}) Sqlizer { } func (r *albumRepository) CountAll(options ...model.QueryOptions) (int64, error) { - return r.count(r.selectAlbum(), options...) + sql := r.selectAlbum() + sql = sql.LeftJoin("album_genres ag on album.id = ag.album_id"). + LeftJoin("genre on ag.genre_id = genre.id"). + GroupBy("album.id") + + return r.count(sql, options...) } func (r *albumRepository) Exists(id string) (bool, error) { diff --git a/persistence/artist_repository.go b/persistence/artist_repository.go index 651f3e353..b00f71587 100644 --- a/persistence/artist_repository.go +++ b/persistence/artist_repository.go @@ -49,7 +49,11 @@ func (r *artistRepository) selectArtist(options ...model.QueryOptions) SelectBui } func (r *artistRepository) CountAll(options ...model.QueryOptions) (int64, error) { - return r.count(r.newSelectWithAnnotation("artist.id"), options...) + sql := r.newSelectWithAnnotation("artist.id") + sql = sql.LeftJoin("artist_genres ag on artist.id = ag.artist_id"). + LeftJoin("genre on ag.genre_id = genre.id"). + GroupBy("artist.id") + return r.count(sql, options...) } func (r *artistRepository) Exists(id string) (bool, error) { diff --git a/persistence/genre_repository.go b/persistence/genre_repository.go index 4ea6f67cf..c5c27a3ab 100644 --- a/persistence/genre_repository.go +++ b/persistence/genre_repository.go @@ -22,6 +22,9 @@ func NewGenreRepository(ctx context.Context, o orm.Ormer) model.GenreRepository r.ctx = ctx r.ormer = o r.tableName = "genre" + r.filterMappings = map[string]filterFunc{ + "name": containsFilter, + } return r } diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go index c2a659e80..f9307a1e1 100644 --- a/persistence/mediafile_repository.go +++ b/persistence/mediafile_repository.go @@ -38,7 +38,11 @@ func NewMediaFileRepository(ctx context.Context, o orm.Ormer) *mediaFileReposito } func (r *mediaFileRepository) CountAll(options ...model.QueryOptions) (int64, error) { - return r.count(r.newSelectWithAnnotation("media_file.id"), options...) + sql := r.newSelectWithAnnotation("media_file.id") + sql = sql.LeftJoin("media_file_genres mfg on media_file.id = mfg.media_file_id"). + LeftJoin("genre on mfg.genre_id = genre.id"). + GroupBy("media_file.id") + return r.count(sql, options...) } func (r *mediaFileRepository) Exists(id string) (bool, error) { diff --git a/persistence/persistence.go b/persistence/persistence.go index 953fbd878..9c816dd16 100644 --- a/persistence/persistence.go +++ b/persistence/persistence.go @@ -88,6 +88,8 @@ func (s *SQLStore) Resource(ctx context.Context, m interface{}) model.ResourceRe return s.Album(ctx).(model.ResourceRepository) case model.MediaFile: return s.MediaFile(ctx).(model.ResourceRepository) + case model.Genre: + return s.Genre(ctx).(model.ResourceRepository) case model.Playlist: return s.Playlist(ctx).(model.ResourceRepository) case model.Share: diff --git a/server/nativeapi/native_api.go b/server/nativeapi/native_api.go index 7c2ffc3af..2f39dd733 100644 --- a/server/nativeapi/native_api.go +++ b/server/nativeapi/native_api.go @@ -37,6 +37,7 @@ func (n *Router) routes() http.Handler { n.R(r, "/song", model.MediaFile{}, true) n.R(r, "/album", model.Album{}, true) n.R(r, "/artist", model.Artist{}, true) + n.R(r, "/genre", model.Genre{}, true) n.R(r, "/player", model.Player{}, true) n.R(r, "/playlist", model.Playlist{}, true) n.R(r, "/transcoding", model.Transcoding{}, conf.Server.EnableTranscodingConfig) diff --git a/ui/src/App.js b/ui/src/App.js index 00f2e05c0..e1a300bb7 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -108,6 +108,7 @@ const Admin = (props) => { ), , + , , , , diff --git a/ui/src/album/AlbumList.js b/ui/src/album/AlbumList.js index 5d2e464cf..f45fbbb6a 100644 --- a/ui/src/album/AlbumList.js +++ b/ui/src/album/AlbumList.js @@ -42,6 +42,15 @@ const AlbumFilter = (props) => { > + ({ name: [searchText] })} + > + + {config.enableFavourites && ( diff --git a/ui/src/artist/ArtistList.js b/ui/src/artist/ArtistList.js index 6e0c3d34c..0a93d72c3 100644 --- a/ui/src/artist/ArtistList.js +++ b/ui/src/artist/ArtistList.js @@ -1,11 +1,14 @@ import React, { useMemo } from 'react' import { useHistory } from 'react-router-dom' import { + AutocompleteInput, Datagrid, Filter, NumberField, + ReferenceInput, SearchInput, TextField, + useTranslate, } from 'react-admin' import { useMediaQuery, withWidth } from '@material-ui/core' import FavoriteIcon from '@material-ui/icons/Favorite' @@ -49,18 +52,30 @@ const useStyles = makeStyles({ }, }) -const ArtistFilter = (props) => ( - - - {config.enableFavourites && ( - } - defaultValue={true} - /> - )} - -) +const ArtistFilter = (props) => { + const translate = useTranslate() + return ( + + + ({ name: [searchText] })} + > + + + {config.enableFavourites && ( + } + defaultValue={true} + /> + )} + + ) +} const ArtistListView = ({ hasShow, hasEdit, hasList, width, ...rest }) => { const classes = useStyles() diff --git a/ui/src/artist/ArtistListActions.js b/ui/src/artist/ArtistListActions.js index 946557670..54fe31765 100644 --- a/ui/src/artist/ArtistListActions.js +++ b/ui/src/artist/ArtistListActions.js @@ -1,13 +1,29 @@ -import React from 'react' +import React, { cloneElement } from 'react' import { sanitizeListRestProps, TopToolbar } from 'react-admin' import { useMediaQuery } from '@material-ui/core' import { ToggleFieldsMenu } from '../common' -const ArtistListActions = ({ className, ...rest }) => { +const ArtistListActions = ({ + className, + filters, + resource, + showFilter, + displayedFilters, + filterValues, + ...rest +}) => { const isNotSmall = useMediaQuery((theme) => theme.breakpoints.up('sm')) return ( + {filters && + cloneElement(filters, { + resource, + showFilter, + displayedFilters, + filterValues, + context: 'button', + })} {isNotSmall && } ) diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json index 7fa83749a..e7069a582 100644 --- a/ui/src/i18n/en.json +++ b/ui/src/i18n/en.json @@ -76,6 +76,7 @@ "albumCount": "Album Count", "songCount": "Song Count", "playCount": "Plays", + "genre": "Genre", "rating": "Rating" } }, diff --git a/ui/src/song/SongList.js b/ui/src/song/SongList.js index 83aca4652..7605013b8 100644 --- a/ui/src/song/SongList.js +++ b/ui/src/song/SongList.js @@ -1,10 +1,13 @@ import React from 'react' import { + AutocompleteInput, Filter, FunctionField, NumberField, + ReferenceInput, SearchInput, TextField, + useTranslate, } from 'react-admin' import { useMediaQuery } from '@material-ui/core' import FavoriteIcon from '@material-ui/icons/Favorite' @@ -55,18 +58,30 @@ const useStyles = makeStyles({ }, }) -const SongFilter = (props) => ( - - - {config.enableFavourites && ( - } - defaultValue={true} - /> - )} - -) +const SongFilter = (props) => { + const translate = useTranslate() + return ( + + + ({ name: [searchText] })} + > + + + {config.enableFavourites && ( + } + defaultValue={true} + /> + )} + + ) +} const SongList = (props) => { const classes = useStyles()