From 98eb1066d8ac8ce0f29bae17ca4c56765a41bd61 Mon Sep 17 00:00:00 2001 From: sentriz Date: Mon, 6 Nov 2023 23:40:00 +0000 Subject: [PATCH] store artist album appearances --- db/db.go | 25 +++++++++++------- db/migrations.go | 35 +++++++++++++++++++++++++ scanner/scanner.go | 21 +++++++++++++++ server/ctrlsubsonic/handlers_by_tags.go | 16 +++++------ 4 files changed, 80 insertions(+), 17 deletions(-) diff --git a/db/db.go b/db/db.go index 68a06b6..8debe2e 100644 --- a/db/db.go +++ b/db/db.go @@ -175,15 +175,17 @@ func (db *DB) SetSetting(key SettingKey, value string) error { } type Artist struct { - ID int `gorm:"primary_key"` - Name string `gorm:"not null; unique_index"` - NameUDec string `sql:"default: null"` - Albums []*Album `gorm:"many2many:album_artists"` - AlbumCount int `sql:"-"` - ArtistStar *ArtistStar - ArtistRating *ArtistRating - AverageRating float64 `sql:"default: null"` - Info *ArtistInfo `gorm:"foreignkey:id"` + ID int `gorm:"primary_key"` + Name string `gorm:"not null; unique_index"` + NameUDec string `sql:"default: null"` + Albums []*Album `gorm:"many2many:album_artists"` + AlbumCount int `sql:"-"` + Appearances []*Album `gorm:"many2many:artist_appearances"` + AppearanceCount int `sql:"-"` + ArtistStar *ArtistStar + ArtistRating *ArtistRating + AverageRating float64 `sql:"default: null"` + Info *ArtistInfo `gorm:"foreignkey:id"` } func (a *Artist) SID() *specid.ID { @@ -395,6 +397,11 @@ type TrackArtist struct { ArtistID int `gorm:"not null; unique_index:idx_track_id_artist_id" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"` } +type ArtistAppearances struct { + ArtistID int `gorm:"not null; unique_index:idx_artist_id_album_id" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"` + AlbumID int `gorm:"not null; unique_index:idx_artist_id_album_id" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` +} + type TrackGenre struct { TrackID int `gorm:"not null; unique_index:idx_track_id_genre_id" sql:"default: null; type:int REFERENCES tracks(id) ON DELETE CASCADE"` GenreID int `gorm:"not null; unique_index:idx_track_id_genre_id" sql:"default: null; type:int REFERENCES genres(id) ON DELETE CASCADE"` diff --git a/db/migrations.go b/db/migrations.go index 8e9d6c6..52b2002 100644 --- a/db/migrations.go +++ b/db/migrations.go @@ -68,6 +68,7 @@ func (db *DB) Migrate(ctx MigrationContext) error { construct(ctx, "202309161411", migratePlaylistsPaths), construct(ctx, "202310252205", migrateAlbumTagArtistString), construct(ctx, "202310281803", migrateTrackArtists), + construct(ctx, "202311062259", migrateArtistAppearances), } return gormigrate. @@ -744,3 +745,37 @@ func migrateTrackArtists(tx *gorm.DB, _ MigrationContext) error { } return tx.AutoMigrate(TrackArtist{}).Error } + +func migrateArtistAppearances(tx *gorm.DB, _ MigrationContext) error { + // gorms seems to want to create the table automatically without ON DELETE rules + step := tx.DropTableIfExists(ArtistAppearances{}) + if err := step.Error; err != nil { + return fmt.Errorf("step drop prev: %w", err) + } + + step = tx.AutoMigrate(ArtistAppearances{}) + if err := step.Error; err != nil { + return fmt.Errorf("step auto migrate: %w", err) + } + + step = tx.Exec(` + INSERT INTO artist_appearances (artist_id, album_id) + SELECT artist_id, album_id + FROM album_artists + `) + if err := step.Error; err != nil { + return fmt.Errorf("step transfer album artists: %w", err) + } + + step = tx.Exec(` + INSERT OR IGNORE INTO artist_appearances (artist_id, album_id) + SELECT track_artists.artist_id, tracks.album_id + FROM track_artists + JOIN tracks ON tracks.id=track_artists.track_id + `) + if err := step.Error; err != nil { + return fmt.Errorf("step transfer album artists: %w", err) + } + + return nil +} diff --git a/scanner/scanner.go b/scanner/scanner.go index 6267001..5002f6d 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -329,6 +329,10 @@ func (s *Scanner) populateTrackAndArtists(tx *db.DB, c *Context, i int, album *d // metadata for the album table comes only from the first track's tags if i == 0 { + if err := tx.Where("album_id=?", album.ID).Delete(db.ArtistAppearances{}).Error; err != nil { + return fmt.Errorf("delete artist appearances: %w", err) + } + albumArtistNames := parseMulti(trags, s.multiValueSettings[AlbumArtist], tagcommon.MustAlbumArtists, tagcommon.MustAlbumArtist) var albumArtistIDs []int for _, albumArtistName := range albumArtistNames { @@ -342,6 +346,10 @@ func (s *Scanner) populateTrackAndArtists(tx *db.DB, c *Context, i int, album *d return fmt.Errorf("populate album artists: %w", err) } + if err := populateArtistAppearances(tx, album, albumArtistIDs); err != nil { + return fmt.Errorf("populate track artists: %w", err) + } + if err := populateAlbum(tx, album, trags, stat.ModTime()); err != nil { return fmt.Errorf("populate album: %w", err) } @@ -371,6 +379,10 @@ func (s *Scanner) populateTrackAndArtists(tx *db.DB, c *Context, i int, album *d return fmt.Errorf("populate track artists: %w", err) } + if err := populateArtistAppearances(tx, album, trackArtistIDs); err != nil { + return fmt.Errorf("populate track artists: %w", err) + } + c.seenTracks[track.ID] = struct{}{} c.seenTracksNew++ @@ -518,6 +530,13 @@ func populateTrackArtists(tx *db.DB, track *db.Track, trackArtistIDs []int) erro return nil } +func populateArtistAppearances(tx *db.DB, album *db.Album, artistIDs []int) error { + if err := tx.InsertBulkLeftMany("artist_appearances", []string{"album_id", "artist_id"}, album.ID, artistIDs); err != nil { + return fmt.Errorf("insert bulk track artists: %w", err) + } + return nil +} + func (s *Scanner) cleanTracks(c *Context) error { start := time.Now() defer func() { log.Printf("finished clean tracks in %s, %d removed", durSince(start), c.TracksMissing()) }() @@ -573,6 +592,8 @@ func (s *Scanner) cleanArtists(c *Context) error { SELECT artist_id FROM track_artists UNION SELECT artist_id FROM album_artists + UNION + SELECT artist_id FROM artist_appearances ) `) if err := q.Error; err != nil { diff --git a/server/ctrlsubsonic/handlers_by_tags.go b/server/ctrlsubsonic/handlers_by_tags.go index 01baea6..ff93faa 100644 --- a/server/ctrlsubsonic/handlers_by_tags.go +++ b/server/ctrlsubsonic/handlers_by_tags.go @@ -26,8 +26,8 @@ func (c *Controller) ServeGetArtists(r *http.Request) *spec.Response { var artists []*db.Artist q := c.dbc. Select("*, count(sub.id) album_count"). - Joins("JOIN album_artists ON album_artists.artist_id=artists.id"). - Joins("JOIN albums sub ON sub.id=album_artists.album_id"). + Joins("JOIN artist_appearances ON artist_appearances.artist_id=artists.id"). + Joins("JOIN albums sub ON sub.id=artist_appearances.album_id"). Preload("ArtistStar", "user_id=?", user.ID). Preload("ArtistRating", "user_id=?", user.ID). Preload("Info"). @@ -69,15 +69,15 @@ func (c *Controller) ServeGetArtist(r *http.Request) *spec.Response { } var artist db.Artist c.dbc. - Preload("Albums", func(db *gorm.DB) *gorm.DB { + Preload("Appearances", func(db *gorm.DB) *gorm.DB { return db. Select("*, count(sub.id) child_count, sum(sub.length) duration"). Joins("LEFT JOIN tracks sub ON albums.id=sub.album_id"). Order("albums.right_path"). Group("albums.id") }). - Preload("Albums.Artists"). - Preload("Albums.Genres"). + Preload("Appearances.Artists"). + Preload("Appearances.Genres"). Preload("Info"). Preload("ArtistStar", "user_id=?", user.ID). Preload("ArtistRating", "user_id=?", user.ID). @@ -85,11 +85,11 @@ func (c *Controller) ServeGetArtist(r *http.Request) *spec.Response { sub := spec.NewResponse() sub.Artist = spec.NewArtistByTags(&artist) - sub.Artist.Albums = make([]*spec.Album, len(artist.Albums)) - for i, album := range artist.Albums { + sub.Artist.Albums = make([]*spec.Album, len(artist.Appearances)) + for i, album := range artist.Appearances { sub.Artist.Albums[i] = spec.NewAlbumByTags(album, album.Artists) } - sub.Artist.AlbumCount = len(artist.Albums) + sub.Artist.AlbumCount = len(artist.Appearances) return sub }