From a74b5a261c5d47c1a24942ecd4ddd98666755ad4 Mon Sep 17 00:00:00 2001 From: sentriz Date: Wed, 9 Feb 2022 17:55:19 +0000 Subject: [PATCH] feat: render local artist images with no foreign key --- server/ctrladmin/handlers.go | 22 +------ server/ctrlbase/ctrl.go | 24 +++++++ server/ctrlsubsonic/handlers_by_tags.go | 65 +++++++++++++------ server/ctrlsubsonic/handlers_raw.go | 25 ++++++- .../ctrlsubsonic/spec/construct_by_folder.go | 6 +- server/ctrlsubsonic/spec/construct_by_tags.go | 6 +- server/db/migrations.go | 27 ++++++++ server/db/model.go | 1 + server/scanner/scanner.go | 14 ++-- server/scanner/scanner_test.go | 19 ++++++ 10 files changed, 160 insertions(+), 49 deletions(-) diff --git a/server/ctrladmin/handlers.go b/server/ctrladmin/handlers.go index 8e94688..10a1bc7 100644 --- a/server/ctrladmin/handlers.go +++ b/server/ctrladmin/handlers.go @@ -16,15 +16,6 @@ import ( "go.senan.xyz/gonic/server/scrobble/listenbrainz" ) -func firstExisting(or string, strings ...string) string { - for _, s := range strings { - if s != "" { - return s - } - } - return or -} - func doScan(scanner *scanner.Scanner, opts scanner.ScanOptions) { go func() { if err := scanner.ScanAndClean(opts); err != nil { @@ -48,18 +39,7 @@ func (c *Controller) ServeHome(r *http.Request) *Response { c.DB.Model(&db.Album{}).Count(&data.AlbumCount) c.DB.Table("tracks").Count(&data.TrackCount) // lastfm box - scheme := firstExisting( - "http", // fallback - r.Header.Get("X-Forwarded-Proto"), - r.Header.Get("X-Forwarded-Scheme"), - r.URL.Scheme, - ) - host := firstExisting( - "localhost:4747", // fallback - r.Header.Get("X-Forwarded-Host"), - r.Host, - ) - data.RequestRoot = fmt.Sprintf("%s://%s", scheme, host) + data.RequestRoot = c.BaseURL(r) data.CurrentLastFMAPIKey, _ = c.DB.GetSetting("lastfm_api_key") data.DefaultListenBrainzURL = listenbrainz.BaseURL // users box diff --git a/server/ctrlbase/ctrl.go b/server/ctrlbase/ctrl.go index a2f91c1..59841cb 100644 --- a/server/ctrlbase/ctrl.go +++ b/server/ctrlbase/ctrl.go @@ -55,6 +55,21 @@ func (c *Controller) Path(rel string) string { return path.Join(c.ProxyPrefix, rel) } +func (c *Controller) BaseURL(r *http.Request) string { + scheme := firstExisting( + "http", // fallback + r.Header.Get("X-Forwarded-Proto"), + r.Header.Get("X-Forwarded-Scheme"), + r.URL.Scheme, + ) + host := firstExisting( + "localhost:4747", // fallback + r.Header.Get("X-Forwarded-Host"), + r.Host, + ) + return fmt.Sprintf("%s://%s", scheme, host) +} + func (c *Controller) WithLogging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // this is (should be) the first middleware. pass right though it @@ -87,3 +102,12 @@ func (c *Controller) WithCORS(next http.Handler) http.Handler { next.ServeHTTP(w, r) }) } + +func firstExisting(or string, strings ...string) string { + for _, s := range strings { + if s != "" { + return s + } + } + return or +} diff --git a/server/ctrlsubsonic/handlers_by_tags.go b/server/ctrlsubsonic/handlers_by_tags.go index 3174832..4ed4486 100644 --- a/server/ctrlsubsonic/handlers_by_tags.go +++ b/server/ctrlsubsonic/handlers_by_tags.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" "net/http" + "net/url" + "strconv" "strings" "github.com/jinzhu/gorm" @@ -248,12 +250,7 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response { if err != nil { return spec.NewError(10, "please provide an `id` parameter") } - apiKey, _ := c.DB.GetSetting("lastfm_api_key") - if apiKey == "" { - sub := spec.NewResponse() - sub.ArtistInfoTwo = &spec.ArtistInfo{} - return sub - } + artist := &db.Artist{} err = c.DB. Where("id=?", id.Value). @@ -262,26 +259,41 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response { if errors.Is(err, gorm.ErrRecordNotFound) { return spec.NewError(70, "artist with id `%s` not found", id) } + + sub := spec.NewResponse() + sub.ArtistInfoTwo = &spec.ArtistInfo{} + if artist.Cover != "" { + sub.ArtistInfoTwo.SmallImageURL = c.genArtistCoverURL(r, artist, 64) + sub.ArtistInfoTwo.MediumImageURL = c.genArtistCoverURL(r, artist, 126) + sub.ArtistInfoTwo.LargeImageURL = c.genArtistCoverURL(r, artist, 256) + } + + apiKey, _ := c.DB.GetSetting("lastfm_api_key") + if apiKey == "" { + return sub + } info, err := lastfm.ArtistGetInfo(apiKey, artist) if err != nil { return spec.NewError(0, "fetching artist info: %v", err) } - sub := spec.NewResponse() - sub.ArtistInfoTwo = &spec.ArtistInfo{ - Biography: info.Bio.Summary, - MusicBrainzID: info.MBID, - LastFMURL: info.URL, - } - for _, image := range info.Image { - switch image.Size { - case "small": - sub.ArtistInfoTwo.SmallImageURL = image.Text - case "medium": - sub.ArtistInfoTwo.MediumImageURL = image.Text - case "large": - sub.ArtistInfoTwo.LargeImageURL = image.Text + + sub.ArtistInfoTwo.Biography = info.Bio.Summary + sub.ArtistInfoTwo.MusicBrainzID = info.MBID + sub.ArtistInfoTwo.LastFMURL = info.URL + + if artist.Cover == "" { + for _, image := range info.Image { + switch image.Size { + case "small": + sub.ArtistInfoTwo.SmallImageURL = image.Text + case "medium": + sub.ArtistInfoTwo.MediumImageURL = image.Text + case "large": + sub.ArtistInfoTwo.LargeImageURL = image.Text + } } } + count := params.GetOrInt("count", 20) inclNotPresent := params.GetOrBool("includeNotPresent", false) for i, similarInfo := range info.Similar.Artists { @@ -310,6 +322,7 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response { sub.ArtistInfoTwo.SimilarArtist = append( sub.ArtistInfoTwo.SimilarArtist, similar) } + return sub } @@ -371,3 +384,15 @@ func (c *Controller) ServeGetStarredTwo(r *http.Request) *spec.Response { } return sub } + +func (c *Controller) genArtistCoverURL(r *http.Request, artist *db.Artist, size int) string { + coverURL, _ := url.Parse(c.BaseURL(r)) + coverURL.Path = c.Path("/rest/getCoverArt") + + query := r.URL.Query() + query.Set("id", artist.SID().String()) + query.Set("size", strconv.Itoa(size)) + coverURL.RawQuery = query.Encode() + + return coverURL.String() +} diff --git a/server/ctrlsubsonic/handlers_raw.go b/server/ctrlsubsonic/handlers_raw.go index e3ba5f5..3342a60 100644 --- a/server/ctrlsubsonic/handlers_raw.go +++ b/server/ctrlsubsonic/handlers_raw.go @@ -78,6 +78,8 @@ func coverGetPath(dbc *db.DB, podcastPath string, id specid.ID) (string, error) switch id.Type { case specid.Album: return coverGetPathAlbum(dbc, id.Value) + case specid.Artist: + return coverGetPathArtist(dbc, id.Value) case specid.Podcast: return coverGetPathPodcast(dbc, podcastPath, id.Value) case specid.PodcastEpisode: @@ -90,7 +92,6 @@ func coverGetPath(dbc *db.DB, podcastPath string, id specid.ID) (string, error) func coverGetPathAlbum(dbc *db.DB, id int) (string, error) { folder := &db.Album{} err := dbc.DB. - Preload("Parent"). Select("id, root_dir, left_path, right_path, cover"). First(folder, id). Error @@ -108,6 +109,28 @@ func coverGetPathAlbum(dbc *db.DB, id int) (string, error) { ), nil } +func coverGetPathArtist(dbc *db.DB, id int) (string, error) { + folder := &db.Album{} + err := dbc.DB.Debug(). + Select("parent.id, parent.root_dir, parent.left_path, parent.right_path, parent.cover"). + Joins("JOIN albums parent ON parent.id=albums.parent_id"). + Where("albums.tag_artist_id=?", id). + Find(folder). + Error + if err != nil { + return "", fmt.Errorf("select guessed artist folder: %w", err) + } + if folder.Cover == "" { + return "", errCoverEmpty + } + return path.Join( + folder.RootDir, + folder.LeftPath, + folder.RightPath, + folder.Cover, + ), nil +} + func coverGetPathPodcast(dbc *db.DB, podcastPath string, id int) (string, error) { podcast := &db.Podcast{} err := dbc. diff --git a/server/ctrlsubsonic/spec/construct_by_folder.go b/server/ctrlsubsonic/spec/construct_by_folder.go index e5ce0a9..3762b0a 100644 --- a/server/ctrlsubsonic/spec/construct_by_folder.go +++ b/server/ctrlsubsonic/spec/construct_by_folder.go @@ -76,11 +76,15 @@ func NewArtistByFolder(f *db.Album) *Artist { // an album is also a folder. so we're constructing an artist // from an "album" where // maybe TODO: rename the Album model to Folder - return &Artist{ + a := &Artist{ ID: f.SID(), Name: f.RightPath, AlbumCount: f.ChildCount, } + if f.Cover != "" { + a.CoverID = f.SID() + } + return a } func NewDirectoryByFolder(f *db.Album, children []*TrackChild) *Directory { diff --git a/server/ctrlsubsonic/spec/construct_by_tags.go b/server/ctrlsubsonic/spec/construct_by_tags.go index 3250995..434f045 100644 --- a/server/ctrlsubsonic/spec/construct_by_tags.go +++ b/server/ctrlsubsonic/spec/construct_by_tags.go @@ -72,11 +72,15 @@ func NewTrackByTags(t *db.Track, album *db.Album) *TrackChild { } func NewArtistByTags(a *db.Artist) *Artist { - return &Artist{ + r := &Artist{ ID: a.SID(), Name: a.Name, AlbumCount: a.AlbumCount, } + if a.Cover != "" { + r.CoverID = a.SID() + } + return r } func NewGenre(g *db.Genre) *Genre { diff --git a/server/db/migrations.go b/server/db/migrations.go index 7a515ac..f284bb3 100644 --- a/server/db/migrations.go +++ b/server/db/migrations.go @@ -37,6 +37,8 @@ func (db *DB) Migrate(ctx MigrationContext) error { construct(ctx, "202102191448", migratePodcastAutoDownload), construct(ctx, "202110041330", migrateAlbumCreatedAt), construct(ctx, "202111021951", migrateAlbumRootDir), + construct(ctx, "202201042236", migrateArtistGuessedFolder), + construct(ctx, "202202092013", migrateArtistCover), } return gormigrate. @@ -306,3 +308,28 @@ func migrateAlbumRootDir(tx *gorm.DB, ctx MigrationContext) error { } return nil } + +func migrateArtistGuessedFolder(tx *gorm.DB, ctx MigrationContext) error { + return tx.AutoMigrate(Artist{}).Error +} + +func migrateArtistCover(tx *gorm.DB, ctx MigrationContext) error { + step := tx.AutoMigrate( + Artist{}, + ) + if err := step.Error; err != nil { + return fmt.Errorf("step auto migrate: %w", err) + } + + if !tx.Dialect().HasColumn("artists", "guessed_folder_id") { + return nil + } + + step = tx.Exec(` + ALTER TABLE artists DROP COLUMN guessed_folder_id + `) + if err := step.Error; err != nil { + return fmt.Errorf("step drop column: %w", err) + } + return nil +} diff --git a/server/db/model.go b/server/db/model.go index 1580192..d9d0d6b 100644 --- a/server/db/model.go +++ b/server/db/model.go @@ -48,6 +48,7 @@ type Artist struct { NameUDec string `sql:"default: null"` Albums []*Album `gorm:"foreignkey:TagArtistID"` AlbumCount int `sql:"-"` + Cover string `sql:"default: null"` } func (a *Artist) SID() *specid.ID { diff --git a/server/scanner/scanner.go b/server/scanner/scanner.go index 17b67b7..f8f22db 100644 --- a/server/scanner/scanner.go +++ b/server/scanner/scanner.go @@ -187,7 +187,7 @@ func (s *Scanner) scanDir(tx *db.DB, c *ctx, musicDir string, absPath string) er sort.Strings(tracks) for i, basename := range tracks { absPath := filepath.Join(musicDir, relPath, basename) - if err := s.populateTrackAndAlbumArtists(tx, c, i, album, basename, absPath); err != nil { + if err := s.populateTrackAndAlbumArtists(tx, c, i, parent, album, basename, absPath); err != nil { return fmt.Errorf("populate track %q: %w", basename, err) } } @@ -195,7 +195,7 @@ func (s *Scanner) scanDir(tx *db.DB, c *ctx, musicDir string, absPath string) er return nil } -func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *ctx, i int, album *db.Album, basename string, absPath string) error { +func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *ctx, i int, parent, album *db.Album, basename string, absPath string) error { track := &db.Track{AlbumID: album.ID, Filename: filepath.Base(basename)} if err := tx.Where(track).First(track).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("query track: %w", err) @@ -217,7 +217,7 @@ func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *ctx, i int, album * } artistName := trags.SomeAlbumArtist() - albumArtist, err := s.populateAlbumArtist(tx, artistName) + albumArtist, err := s.populateAlbumArtist(tx, parent, artistName) if err != nil { return fmt.Errorf("populate artist: %w", err) } @@ -315,12 +315,15 @@ func populateTrack(tx *db.DB, album *db.Album, albumArtist *db.Artist, track *db return nil } -func (s *Scanner) populateAlbumArtist(tx *db.DB, artistName string) (*db.Artist, error) { +func (s *Scanner) populateAlbumArtist(tx *db.DB, parent *db.Album, artistName string) (*db.Artist, error) { var artist db.Artist update := db.Artist{ Name: artistName, NameUDec: decoded(artistName), } + if parent.Cover != "" { + update.Cover = parent.Cover + } if err := tx.Where("name=?", artistName).Assign(update).FirstOrCreate(&artist).Error; err != nil { return nil, fmt.Errorf("find or create artist: %w", err) } @@ -476,7 +479,8 @@ func isCover(name string) bool { "folder.png", "folder.jpg", "folder.jpeg", "album.png", "album.jpg", "album.jpeg", "albumart.png", "albumart.jpg", "albumart.jpeg", - "front.png", "front.jpg", "front.jpeg": + "front.png", "front.jpg", "front.jpeg", + "artist.png", "artist.jpg", "artist.jpeg": return true default: return false diff --git a/server/scanner/scanner_test.go b/server/scanner/scanner_test.go index 4a6b5ba..1c128a6 100644 --- a/server/scanner/scanner_test.go +++ b/server/scanner/scanner_test.go @@ -441,3 +441,22 @@ func TestSymlinkedSubdiscs(t *testing.T) { is.True(!info.IsDir()) // track resolves is.True(!info.ModTime().IsZero()) // track resolves } + +func TestArtistHasCover(t *testing.T) { + t.Parallel() + is := is.New(t) + m := mockfs.New(t) + defer m.CleanUp() + + m.AddItemsWithCovers() + m.AddCover("artist-2/artist.png") + m.ScanAndClean() + + var artistWith db.Artist + is.NoErr(m.DB().Where("name=?", "artist-2").First(&artistWith).Error) + is.Equal(artistWith.Cover, "artist.png") + + var artistWithout db.Artist + is.NoErr(m.DB().Where("name=?", "artist-0").First(&artistWithout).Error) + is.Equal(artistWithout.Cover, "") +}