feat(subsonic): add support for multi-valued album artist tags

closes #103

a

a

a

r

a

a

a

a

a

a

a

a

a

a
This commit is contained in:
sentriz
2023-07-31 23:07:41 +01:00
parent 908c7cf088
commit 3ac77823c3
27 changed files with 641 additions and 266 deletions

View File

@@ -360,14 +360,24 @@ func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *Context, i int, par
}
// metadata for the album table comes only from the the first track's tags
if i == 0 || album.TagArtist == nil {
albumArtist, err := populateAlbumArtist(tx, parent, tags.MustAlbumArtist(trags))
if err != nil {
return fmt.Errorf("populate album artist: %w", err)
if i == 0 {
albumArtists := tags.MustAlbumArtists(trags)
var albumArtistIDs []int
for _, albumArtistName := range albumArtists {
albumArtist, err := populateArtist(tx, parent, albumArtistName)
if err != nil {
return fmt.Errorf("populate album artist: %w", err)
}
albumArtistIDs = append(albumArtistIDs, albumArtist.ID)
}
if err := populateAlbum(tx, album, albumArtist, trags, stat.ModTime()); err != nil {
if err := populateAlbumArtists(tx, album, albumArtistIDs); err != nil {
return fmt.Errorf("populate album artists: %w", err)
}
if err := populateAlbum(tx, album, trags, stat.ModTime()); err != nil {
return fmt.Errorf("populate album: %w", err)
}
if err := populateAlbumGenres(tx, album, genreIDs); err != nil {
return fmt.Errorf("populate album genres: %w", err)
}
@@ -386,13 +396,12 @@ func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *Context, i int, par
return nil
}
func populateAlbum(tx *db.DB, album *db.Album, albumArtist *db.Artist, trags tags.Parser, modTime time.Time) error {
func populateAlbum(tx *db.DB, album *db.Album, trags tags.Parser, modTime time.Time) error {
albumName := tags.MustAlbum(trags)
album.TagTitle = albumName
album.TagTitleUDec = decoded(albumName)
album.TagBrainzID = trags.AlbumBrainzID()
album.TagYear = trags.Year()
album.TagArtist = albumArtist
album.ModifiedAt = modTime
album.CreatedAt = modTime
@@ -434,7 +443,6 @@ func populateTrack(tx *db.DB, album *db.Album, track *db.Track, trags tags.Parse
track.FilenameUDec = decoded(basename)
track.Size = size
track.AlbumID = album.ID
track.ArtistID = album.TagArtist.ID
track.TagTitle = trags.Title()
track.TagTitleUDec = decoded(trags.Title())
@@ -453,7 +461,7 @@ func populateTrack(tx *db.DB, album *db.Album, track *db.Track, trags tags.Parse
return nil
}
func populateAlbumArtist(tx *db.DB, parent *db.Album, artistName string) (*db.Artist, error) {
func populateArtist(tx *db.DB, parent *db.Album, artistName string) (*db.Artist, error) {
var update db.Artist
update.Name = artistName
update.NameUDec = decoded(artistName)
@@ -510,6 +518,17 @@ func populateAlbumGenres(tx *db.DB, album *db.Album, genreIDs []int) error {
return nil
}
func populateAlbumArtists(tx *db.DB, album *db.Album, albumArtistIDs []int) error {
if err := tx.Where("album_id=?", album.ID).Delete(db.AlbumArtist{}).Error; err != nil {
return fmt.Errorf("delete old album album artists: %w", err)
}
if err := tx.InsertBulkLeftMany("album_artists", []string{"album_id", "artist_id"}, album.ID, albumArtistIDs); err != nil {
return fmt.Errorf("insert bulk album 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()) }()
@@ -561,8 +580,8 @@ func (s *Scanner) cleanArtists(c *Context) error {
sub := s.db.
Select("artists.id").
Model(&db.Artist{}).
Joins("LEFT JOIN albums ON albums.tag_artist_id=artists.id").
Where("albums.id IS NULL").
Joins("LEFT JOIN album_artists ON album_artists.artist_id=artists.id").
Where("album_artists.artist_id IS NULL").
SubQuery()
q := s.db.
Where("artists.id IN ?", sub).

View File

@@ -115,9 +115,12 @@ func TestCoverBeforeTracks(t *testing.T) {
m.ScanAndClean()
var album db.Album
require.NoError(m.DB().Preload("TagArtist").Where("left_path=? AND right_path=?", "artist-2/", "album-2").Find(&album).Error) // album has cover
require.Equal("cover.jpg", album.Cover) // album has cover
require.Equal("artist-2", album.TagArtist.Name) // album artist
require.NoError(m.DB().Where("left_path=? AND right_path=?", "artist-2/", "album-2").Find(&album).Error) // album has cover
require.Equal("cover.jpg", album.Cover) // album has cover
var albumArtist db.Artist
require.NoError(m.DB().Joins("JOIN album_artists ON album_artists.artist_id=artists.id").Where("album_artists.album_id=?", album.ID).Find(&albumArtist).Error) // album has cover
require.Equal("artist-2", albumArtist.Name) // album artist
var tracks []*db.Track
require.NoError(m.DB().Where("album_id=?", album.ID).Find(&tracks).Error) // album has tracks
@@ -141,11 +144,14 @@ func TestUpdatedTags(t *testing.T) {
m.ScanAndClean()
var track db.Track
require.NoError(m.DB().Preload("Album").Preload("Artist").Where("filename=?", "track-10.flac").Find(&track).Error) // track has tags
require.Equal("artist", track.TagTrackArtist) // track has tags
require.Equal("album-artist", track.Artist.Name) // track has tags
require.Equal("album", track.Album.TagTitle) // track has tags
require.Equal("title", track.TagTitle) // track has tags
require.NoError(m.DB().Preload("Album").Where("filename=?", "track-10.flac").Find(&track).Error) // track has tags
require.Equal("artist", track.TagTrackArtist) // track has tags
require.Equal("album", track.Album.TagTitle) // track has tags
require.Equal("title", track.TagTitle) // track has tags
var trackArtistA db.Artist
require.NoError(m.DB().Joins("JOIN album_artists ON album_artists.artist_id=artists.id").Where("album_artists.album_id=?", track.AlbumID).Limit(1).Find(&trackArtistA).Error) // updated has tags
require.Equal("album-artist", trackArtistA.Name) // track has tags
m.SetTags("artist-10/album-10/track-10.flac", func(tags *mockfs.Tags) error {
tags.RawArtist = "artist-upd"
@@ -158,12 +164,15 @@ func TestUpdatedTags(t *testing.T) {
m.ScanAndClean()
var updated db.Track
require.NoError(m.DB().Preload("Album").Preload("Artist").Where("filename=?", "track-10.flac").Find(&updated).Error) // updated has tags
require.Equal(track.ID, updated.ID) // updated has tags
require.Equal("artist-upd", updated.TagTrackArtist) // updated has tags
require.Equal("album-artist-upd", updated.Artist.Name) // updated has tags
require.Equal("album-upd", updated.Album.TagTitle) // updated has tags
require.Equal("title-upd", updated.TagTitle) // updated has tags
require.NoError(m.DB().Preload("Album").Where("filename=?", "track-10.flac").Find(&updated).Error) // updated has tags
require.Equal(track.ID, updated.ID) // updated has tags
require.Equal("artist-upd", updated.TagTrackArtist) // updated has tags
require.Equal("album-upd", updated.Album.TagTitle) // updated has tags
require.Equal("title-upd", updated.TagTitle) // updated has tags
var trackArtistB db.Artist
require.NoError(m.DB().Joins("JOIN album_artists ON album_artists.artist_id=artists.id").Where("album_artists.album_id=?", track.AlbumID).Limit(1).Find(&trackArtistB).Error) // updated has tags
require.Equal("album-artist-upd", trackArtistB.Name) // updated has tags
}
// https://github.com/sentriz/gonic/issues/225
@@ -409,21 +418,22 @@ func TestMultiFolderWithSharedArtist(t *testing.T) {
})
m.ScanAndClean()
sq := 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").
Group("albums.id")
}
var artist db.Artist
require.NoError(m.DB().Where("name=?", artistName).Preload("Albums", sq).First(&artist).Error)
require.NoError(m.DB().Where("name=?", artistName).First(&artist).Error)
require.Equal(artistName, artist.Name)
require.Equal(2, len(artist.Albums))
for _, album := range artist.Albums {
var artistAlbums []*db.Album
require.NoError(m.DB().
Select("*, count(sub.id) child_count, sum(sub.length) duration").
Joins("JOIN album_artists ON album_artists.album_id=albums.id").
Joins("LEFT JOIN tracks sub ON albums.id=sub.album_id").
Where("album_artists.artist_id=?", artist.ID).
Group("albums.id").
Find(&artistAlbums).Error)
require.Equal(2, len(artistAlbums))
for _, album := range artistAlbums {
require.Greater(album.TagYear, 0)
require.Equal(artist.ID, album.TagArtistID)
require.Greater(album.ChildCount, 0)
require.Greater(album.Duration, 0)
}
@@ -574,12 +584,15 @@ func TestCompilationAlbumWithoutAlbumArtist(t *testing.T) {
require.Equal(5, trackCount)
var artists []*db.Artist
require.NoError(m.DB().Preload("Albums").Find(&artists).Error)
require.NoError(m.DB().Find(&artists).Error)
require.Equal(1, len(artists)) // we only have one album artist
require.Equal("artist 0", artists[0].Name) // it came from the first track's fallback to artist tag
require.Equal(1, len(artists[0].Albums)) // the artist has one album
require.Equal(pathAlbum, artists[0].Albums[0].RightPath)
require.Equal(pathArtist+"/", artists[0].Albums[0].LeftPath)
var artistAlbums []*db.Album
require.NoError(m.DB().Joins("JOIN album_artists ON album_artists.album_id=albums.id").Where("album_artists.artist_id=?", artists[0].ID).Find(&artistAlbums).Error)
require.Equal(1, len(artistAlbums)) // the artist has one album
require.Equal(pathAlbum, artistAlbums[0].RightPath)
require.Equal(pathArtist+"/", artistAlbums[0].LeftPath)
}
func TestIncrementalScanNoChangeNoUpdatedAt(t *testing.T) {
@@ -591,11 +604,11 @@ func TestIncrementalScanNoChangeNoUpdatedAt(t *testing.T) {
m.ScanAndClean()
var albumA db.Album
require.NoError(m.DB().Where("tag_artist_id NOT NULL").Order("updated_at DESC").Find(&albumA).Error)
require.NoError(m.DB().Joins("JOIN album_artists ON album_artists.album_id=albums.id").Order("updated_at DESC").Find(&albumA).Error)
m.ScanAndClean()
var albumB db.Album
require.NoError(m.DB().Where("tag_artist_id NOT NULL").Order("updated_at DESC").Find(&albumB).Error)
require.NoError(m.DB().Joins("JOIN album_artists ON album_artists.album_id=albums.id").Order("updated_at DESC").Find(&albumB).Error)
require.Equal(albumB.UpdatedAt, albumA.UpdatedAt)
}
@@ -646,3 +659,139 @@ func TestNoOrphanedGenres(t *testing.T) {
require.NoError(m.DB().Model(&db.Genre{}).Count(&genreCount).Error)
require.Equal(0, genreCount)
}
func TestMultiArtistSupport(t *testing.T) {
t.Parallel()
require := assert.New(t)
m := mockfs.New(t)
m.AddItemsGlob("artist-0/album-[012]/track-0.*")
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.Tags) error {
tags.RawAlbum = "Mutator"
tags.RawAlbumArtists = []string{"Alan Vega", "Liz Lamere"}
return nil
})
m.SetTags("artist-0/album-1/track-0.flac", func(tags *mockfs.Tags) error {
tags.RawAlbum = "Dead Man"
tags.RawAlbumArtists = []string{"Alan Vega", "Mercury Rev"}
return nil
})
m.SetTags("artist-0/album-2/track-0.flac", func(tags *mockfs.Tags) error {
tags.RawAlbum = "Yerself Is Steam"
tags.RawAlbumArtist = "Mercury Rev"
return nil
})
m.ScanAndClean()
var artists []*db.Artist
require.NoError(m.DB().Find(&artists).Error)
require.Len(artists, 3) // alan, liz, mercury
var albumArtists []*db.AlbumArtist
require.NoError(m.DB().Find(&albumArtists).Error)
require.Len(albumArtists, 5)
type row struct{ Artist, Albums string }
state := func() []row {
var table []row
require.NoError(m.DB().
Select("artists.name artist, group_concat(albums.tag_title, ';') albums").
Model(db.Artist{}).
Joins("JOIN album_artists ON album_artists.artist_id=artists.id").
Joins("JOIN albums ON albums.id=album_artists.album_id").
Order("artists.name, albums.tag_title").
Group("artists.id").
Scan(&table).
Error)
return table
}
require.Equal(
[]row{
{"Alan Vega", "Mutator;Dead Man"},
{"Liz Lamere", "Mutator"},
{"Mercury Rev", "Dead Man;Yerself Is Steam"},
},
state(),
)
m.RemoveAll("artist-0/album-2")
m.SetTags("artist-0/album-1/track-0.flac", func(tags *mockfs.Tags) error {
tags.RawAlbum = "Dead Man"
tags.RawAlbumArtists = []string{"Alan Vega"}
return nil
})
m.ScanAndClean()
require.NoError(m.DB().Find(&artists).Error)
require.Len(artists, 2) // alan, liz
require.NoError(m.DB().Find(&albumArtists).Error)
require.Len(albumArtists, 3)
require.Equal(
[]row{
{"Alan Vega", "Mutator;Dead Man"},
{"Liz Lamere", "Mutator"},
},
state(),
)
}
func TestMultiArtistPreload(t *testing.T) {
t.Parallel()
require := assert.New(t)
m := mockfs.New(t)
m.AddItemsGlob("artist-0/album-[012]/track-0.*")
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.Tags) error {
tags.RawAlbum = "Mutator"
tags.RawAlbumArtists = []string{"Alan Vega", "Liz Lamere"}
return nil
})
m.SetTags("artist-0/album-1/track-0.flac", func(tags *mockfs.Tags) error {
tags.RawAlbum = "Dead Man"
tags.RawAlbumArtists = []string{"Alan Vega", "Mercury Rev"}
return nil
})
m.SetTags("artist-0/album-2/track-0.flac", func(tags *mockfs.Tags) error {
tags.RawAlbum = "Yerself Is Steam"
tags.RawAlbumArtist = "Mercury Rev"
return nil
})
m.ScanAndClean()
var albums []*db.Album
require.NoError(m.DB().Preload("Artists").Find(&albums).Error)
require.GreaterOrEqual(len(albums), 3)
for _, album := range albums {
switch album.TagTitle {
case "Mutator":
require.Len(album.Artists, 2)
case "Dead Man":
require.Len(album.Artists, 2)
case "Yerself Is Steam":
require.Len(album.Artists, 1)
}
}
var artists []*db.Artist
require.NoError(m.DB().Preload("Albums").Find(&artists).Error)
require.Equal(3, len(artists))
for _, artist := range artists {
switch artist.Name {
case "Alan Vega":
require.Len(artist.Albums, 2)
case "Mercury Rev":
require.Len(artist.Albums, 2)
case "Liz Lamere":
require.Len(artist.Albums, 1)
}
}
}

View File

@@ -127,16 +127,6 @@ func MustArtist(p Parser) string {
return "Unknown Artist"
}
func MustAlbumArtist(p Parser) string {
if r := p.AlbumArtist(); r != "" {
return r
}
if r := p.Artist(); r != "" {
return r
}
return "Unknown Artist"
}
func MustAlbumArtists(p Parser) []string {
if r := p.AlbumArtists(); len(r) > 0 {
return r