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:
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user