From 100db2bcfdba0d9efa0e2a8e2de1c9567c584895 Mon Sep 17 00:00:00 2001 From: Deluan Date: Wed, 25 Mar 2020 18:51:13 -0400 Subject: [PATCH] feat: add artist filter to album view --- .../20200325185135_add_album_artist_id.go | 33 ++++++++++++++ model/album.go | 33 +++++++------- model/mediafile.go | 43 ++++++++++--------- persistence/album_repository.go | 14 ++++-- persistence/artist_repository.go | 8 ++-- persistence/helpers.go | 7 +++ persistence/mock_album_repo.go | 2 +- persistence/persistence_suite_test.go | 6 +-- scanner/tag_scanner.go | 24 +++++++---- ui/src/album/AlbumList.js | 14 +++++- ui/src/artist/ArtistList.js | 4 +- 11 files changed, 129 insertions(+), 59 deletions(-) create mode 100644 db/migration/20200325185135_add_album_artist_id.go diff --git a/db/migration/20200325185135_add_album_artist_id.go b/db/migration/20200325185135_add_album_artist_id.go new file mode 100644 index 00000000..23d0cb7d --- /dev/null +++ b/db/migration/20200325185135_add_album_artist_id.go @@ -0,0 +1,33 @@ +package migration + +import ( + "database/sql" + "github.com/pressly/goose" +) + +func init() { + goose.AddMigration(Up20200325185135, Down20200325185135) +} + +func Up20200325185135(tx *sql.Tx) error { + _, err := tx.Exec(` +alter table album + add album_artist_id varchar(255) default ''; +create index album_artist_album_id + on album (album_artist_id); + +alter table media_file + add album_artist_id varchar(255) default ''; +create index media_file_artist_album_id + on media_file (album_artist_id); +`) + if err != nil { + return err + } + notice(tx, "A full rescan will be performed!") + return forceFullRescan(tx) +} + +func Down20200325185135(tx *sql.Tx) error { + return nil +} diff --git a/model/album.go b/model/album.go index 2bc263e7..da873245 100644 --- a/model/album.go +++ b/model/album.go @@ -3,21 +3,22 @@ package model import "time" type Album struct { - ID string `json:"id" orm:"column(id)"` - Name string `json:"name"` - ArtistID string `json:"artistId" orm:"pk;column(artist_id)"` - CoverArtPath string `json:"coverArtPath"` - CoverArtId string `json:"coverArtId"` - Artist string `json:"artist"` - AlbumArtist string `json:"albumArtist"` - Year int `json:"year"` - Compilation bool `json:"compilation"` - SongCount int `json:"songCount"` - Duration float32 `json:"duration"` - Genre string `json:"genre"` - FullText string `json:"fullText"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + ID string `json:"id" orm:"column(id)"` + Name string `json:"name"` + CoverArtPath string `json:"coverArtPath"` + CoverArtId string `json:"coverArtId"` + ArtistID string `json:"artistId" orm:"pk;column(artist_id)"` + Artist string `json:"artist"` + AlbumArtistID string `json:"albumArtistId" orm:"pk;column(album_artist_id)"` + AlbumArtist string `json:"albumArtist"` + Year int `json:"year"` + Compilation bool `json:"compilation"` + SongCount int `json:"songCount"` + Duration float32 `json:"duration"` + Genre string `json:"genre"` + FullText string `json:"fullText"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` // Annotations PlayCount int `json:"-" orm:"-"` @@ -34,7 +35,7 @@ type AlbumRepository interface { Exists(id string) (bool, error) Put(m *Album) error Get(id string) (*Album, error) - FindByArtist(artistId string) (Albums, error) + FindByArtist(albumArtistId string) (Albums, error) GetAll(...QueryOptions) (Albums, error) GetRandom(...QueryOptions) (Albums, error) GetStarred(options ...QueryOptions) (Albums, error) diff --git a/model/mediafile.go b/model/mediafile.go index b93cf13d..5b8e7907 100644 --- a/model/mediafile.go +++ b/model/mediafile.go @@ -6,27 +6,28 @@ import ( ) type MediaFile struct { - ID string `json:"id" orm:"pk;column(id)"` - Path string `json:"path"` - Title string `json:"title"` - Album string `json:"album"` - Artist string `json:"artist"` - ArtistID string `json:"artistId" orm:"pk;column(artist_id)"` - AlbumArtist string `json:"albumArtist"` - AlbumID string `json:"albumId" orm:"pk;column(album_id)"` - HasCoverArt bool `json:"hasCoverArt"` - TrackNumber int `json:"trackNumber"` - DiscNumber int `json:"discNumber"` - Year int `json:"year"` - Size int `json:"size"` - Suffix string `json:"suffix"` - Duration float32 `json:"duration"` - BitRate int `json:"bitRate"` - Genre string `json:"genre"` - FullText string `json:"fullText"` - Compilation bool `json:"compilation"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + ID string `json:"id" orm:"pk;column(id)"` + Path string `json:"path"` + Title string `json:"title"` + Album string `json:"album"` + ArtistID string `json:"artistId" orm:"pk;column(artist_id)"` + Artist string `json:"artist"` + AlbumArtistID string `json:"albumArtistId"` + AlbumArtist string `json:"albumArtist"` + AlbumID string `json:"albumId" orm:"pk;column(album_id)"` + HasCoverArt bool `json:"hasCoverArt"` + TrackNumber int `json:"trackNumber"` + DiscNumber int `json:"discNumber"` + Year int `json:"year"` + Size int `json:"size"` + Suffix string `json:"suffix"` + Duration float32 `json:"duration"` + BitRate int `json:"bitRate"` + Genre string `json:"genre"` + FullText string `json:"fullText"` + Compilation bool `json:"compilation"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` // Annotations PlayCount int `json:"-" orm:"-"` diff --git a/persistence/album_repository.go b/persistence/album_repository.go index 5c3fa6ad..44bfbf09 100644 --- a/persistence/album_repository.go +++ b/persistence/album_repository.go @@ -28,11 +28,19 @@ func NewAlbumRepository(ctx context.Context, o orm.Ormer) model.AlbumRepository r.filterMappings = map[string]filterFunc{ "name": fullTextFilter, "compilation": booleanFilter, + "artist_id": artistFilter, } return r } +func artistFilter(field string, value interface{}) Sqlizer { + return Or{ + exist("from media_file where album.id = media_file.album_id and media_file.artist_id='" + value.(string) + "'"), + exist("from media_file where album.id = media_file.album_id and media_file.album_artist_id='" + value.(string) + "'"), + } +} + func (r *albumRepository) CountAll(options ...model.QueryOptions) (int64, error) { return r.count(Select(), options...) } @@ -62,7 +70,7 @@ func (r *albumRepository) Get(id string) (*model.Album, error) { } func (r *albumRepository) FindByArtist(artistId string) (model.Albums, error) { - sq := r.selectAlbum().Where(Eq{"artist_id": artistId}).OrderBy("year") + sq := r.selectAlbum().Where(Eq{"album_artist_id": artistId}).OrderBy("year") res := model.Albums{} err := r.queryAll(sq, &res) return res, err @@ -91,8 +99,8 @@ func (r *albumRepository) Refresh(ids ...string) error { HasCoverArt bool } var albums []refreshAlbum - sel := Select(`album_id as id, album as name, f.artist, f.album_artist, f.artist_id, f.compilation, f.genre, - max(f.year) as year, sum(f.duration) as duration, count(*) as song_count, a.id as current_id, + sel := Select(`album_id as id, album as name, f.artist, f.album_artist, f.artist_id, f.album_artist_id, + f.compilation, f.genre, max(f.year) as year, sum(f.duration) as duration, count(*) as song_count, a.id as current_id, f.id as cover_art_id, f.path as cover_art_path, f.has_cover_art`). From("media_file f"). LeftJoin("album a on f.album_id = a.id"). diff --git a/persistence/artist_repository.go b/persistence/artist_repository.go index 256ef330..afddef7b 100644 --- a/persistence/artist_repository.go +++ b/persistence/artist_repository.go @@ -114,12 +114,12 @@ func (r *artistRepository) Refresh(ids ...string) error { Compilation bool } var artists []refreshArtist - sel := Select("f.artist_id as id", "f.artist as name", "f.album_artist", "f.compilation", + sel := Select("f.album_artist_id as id", "f.artist as name", "f.album_artist", "f.compilation", "count(*) as album_count", "a.id as current_id"). From("album f"). - LeftJoin("artist a on f.artist_id = a.id"). - Where(Eq{"f.artist_id": ids}). - GroupBy("f.artist_id").OrderBy("f.id") + LeftJoin("artist a on f.album_artist_id = a.id"). + Where(Eq{"f.album_artist_id": ids}). + GroupBy("f.album_artist_id").OrderBy("f.id") err := r.queryAll(sel, &artists) if err != nil { return err diff --git a/persistence/helpers.go b/persistence/helpers.go index b443a807..d0b1aff4 100644 --- a/persistence/helpers.go +++ b/persistence/helpers.go @@ -60,3 +60,10 @@ func toCamelCase(str string) string { return strings.ToUpper(strings.Replace(s, "_", "", -1)) }) } + +type exist string + +func (e exist) ToSql() (string, []interface{}, error) { + sql := fmt.Sprintf("exists (select 1 %s)", e) + return sql, nil, nil +} diff --git a/persistence/mock_album_repo.go b/persistence/mock_album_repo.go index 6af1b948..1b014c5e 100644 --- a/persistence/mock_album_repo.go +++ b/persistence/mock_album_repo.go @@ -68,7 +68,7 @@ func (m *MockAlbum) FindByArtist(artistId string) (model.Albums, error) { var res = make(model.Albums, len(m.data)) i := 0 for _, a := range m.data { - if a.ArtistID == artistId { + if a.AlbumArtistID == artistId { res[i] = *a i++ } diff --git a/persistence/persistence_suite_test.go b/persistence/persistence_suite_test.go index 3567e7ad..ebad4a10 100644 --- a/persistence/persistence_suite_test.go +++ b/persistence/persistence_suite_test.go @@ -40,9 +40,9 @@ var ( ) var ( - albumSgtPeppers = model.Album{ID: "1", Name: "Sgt Peppers", Artist: "The Beatles", ArtistID: "3", Genre: "Rock", CoverArtId: "1", CoverArtPath: P("/beatles/1/sgt/a day.mp3"), SongCount: 1, Year: 1967, FullText: "sgt peppers the beatles"} - albumAbbeyRoad = model.Album{ID: "2", Name: "Abbey Road", Artist: "The Beatles", ArtistID: "3", Genre: "Rock", CoverArtId: "2", CoverArtPath: P("/beatles/1/come together.mp3"), SongCount: 1, Year: 1969, FullText: "abbey road the beatles"} - albumRadioactivity = model.Album{ID: "3", Name: "Radioactivity", Artist: "Kraftwerk", ArtistID: "2", Genre: "Electronic", CoverArtId: "3", CoverArtPath: P("/kraft/radio/radio.mp3"), SongCount: 2, FullText: "radioactivity kraftwerk"} + albumSgtPeppers = model.Album{ID: "1", Name: "Sgt Peppers", Artist: "The Beatles", AlbumArtistID: "3", Genre: "Rock", CoverArtId: "1", CoverArtPath: P("/beatles/1/sgt/a day.mp3"), SongCount: 1, Year: 1967, FullText: "sgt peppers the beatles"} + albumAbbeyRoad = model.Album{ID: "2", Name: "Abbey Road", Artist: "The Beatles", AlbumArtistID: "3", Genre: "Rock", CoverArtId: "2", CoverArtPath: P("/beatles/1/come together.mp3"), SongCount: 1, Year: 1969, FullText: "abbey road the beatles"} + albumRadioactivity = model.Album{ID: "3", Name: "Radioactivity", Artist: "Kraftwerk", AlbumArtistID: "2", Genre: "Electronic", CoverArtId: "3", CoverArtPath: P("/kraft/radio/radio.mp3"), SongCount: 2, FullText: "radioactivity kraftwerk"} testAlbums = model.Albums{ albumSgtPeppers, albumAbbeyRoad, diff --git a/scanner/tag_scanner.go b/scanner/tag_scanner.go index d01ca46c..9572f0f1 100644 --- a/scanner/tag_scanner.go +++ b/scanner/tag_scanner.go @@ -241,13 +241,10 @@ func (s *TagScanner) toMediaFile(md *Metadata) model.MediaFile { mf.Album = md.Album() mf.AlbumID = s.albumID(md) mf.Album = s.mapAlbumName(md) - if md.Artist() == "" { - mf.Artist = consts.UnknownArtist - } else { - mf.Artist = md.Artist() - } mf.ArtistID = s.artistID(md) - mf.AlbumArtist = md.AlbumArtist() + mf.Artist = s.mapArtistName(md) + mf.AlbumArtistID = s.albumArtistID(md) + mf.AlbumArtist = s.mapAlbumArtistName(md) mf.Genre = md.Genre() mf.Compilation = md.Compilation() mf.Year = md.Year() @@ -276,7 +273,7 @@ func (s *TagScanner) mapTrackTitle(md *Metadata) string { return md.Title() } -func (s *TagScanner) mapArtistName(md *Metadata) string { +func (s *TagScanner) mapAlbumArtistName(md *Metadata) string { switch { case md.Compilation(): return consts.VariousArtists @@ -289,6 +286,13 @@ func (s *TagScanner) mapArtistName(md *Metadata) string { } } +func (s *TagScanner) mapArtistName(md *Metadata) string { + if md.Artist() != "" { + return md.Artist() + } + return consts.UnknownArtist +} + func (s *TagScanner) mapAlbumName(md *Metadata) string { name := md.Album() if name == "" { @@ -302,10 +306,14 @@ func (s *TagScanner) trackID(md *Metadata) string { } func (s *TagScanner) albumID(md *Metadata) string { - albumPath := strings.ToLower(fmt.Sprintf("%s\\%s", s.mapArtistName(md), s.mapAlbumName(md))) + albumPath := strings.ToLower(fmt.Sprintf("%s\\%s", s.mapAlbumArtistName(md), s.mapAlbumName(md))) return fmt.Sprintf("%x", md5.Sum([]byte(albumPath))) } func (s *TagScanner) artistID(md *Metadata) string { return fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(s.mapArtistName(md))))) } + +func (s *TagScanner) albumArtistID(md *Metadata) string { + return fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(s.mapAlbumArtistName(md))))) +} diff --git a/ui/src/album/AlbumList.js b/ui/src/album/AlbumList.js index a7ba9917..b357619b 100644 --- a/ui/src/album/AlbumList.js +++ b/ui/src/album/AlbumList.js @@ -9,9 +9,11 @@ import { FunctionField, SearchInput, NumberInput, - BooleanInput, + NullableBooleanInput, Show, SimpleShowLayout, + ReferenceInput, + AutocompleteInput, TextField } from 'react-admin' import { DurationField, Pagination, Title } from '../common' @@ -20,7 +22,15 @@ import { useMediaQuery } from '@material-ui/core' const AlbumFilter = (props) => ( - + ({ name: [searchText] })} + > + + + ) diff --git a/ui/src/artist/ArtistList.js b/ui/src/artist/ArtistList.js index 7eef133d..f33a08cb 100644 --- a/ui/src/artist/ArtistList.js +++ b/ui/src/artist/ArtistList.js @@ -17,7 +17,9 @@ const ArtistFilter = (props) => ( const artistRowClick = (id, basePath, record) => { const filter = { artist_id: id } - return `/album?filter=${JSON.stringify(filter)}&order=ASC&sort=year` + return `/album?filter=${JSON.stringify( + filter + )}&order=ASC&sort=year&displayedFilters={"compilation":true}` } const ArtistList = (props) => (