Add inital multiple artist support

This commit is contained in:
sentriz
2020-12-15 23:26:13 +00:00
committed by Senan Kelly
parent f71c345ba1
commit de79b043e1
10 changed files with 160 additions and 91 deletions

View File

@@ -34,6 +34,7 @@ func main() {
confScanInterval := set.Int("scan-interval", 0, "interval (in minutes) to automatically scan music (optional)") confScanInterval := set.Int("scan-interval", 0, "interval (in minutes) to automatically scan music (optional)")
confJukeboxEnabled := set.Bool("jukebox-enabled", false, "whether the subsonic jukebox api should be enabled (optional)") confJukeboxEnabled := set.Bool("jukebox-enabled", false, "whether the subsonic jukebox api should be enabled (optional)")
confProxyPrefix := set.String("proxy-prefix", "", "url path prefix to use if behind proxy. eg '/gonic' (optional)") confProxyPrefix := set.String("proxy-prefix", "", "url path prefix to use if behind proxy. eg '/gonic' (optional)")
confGenreSplit := set.String("genre-split", "\n", "character or string to split genre tag data on (optional)")
confShowVersion := set.Bool("version", false, "show gonic version") confShowVersion := set.Bool("version", false, "show gonic version")
_ = set.String("config-path", "", "path to config (optional)") _ = set.String("config-path", "", "path to config (optional)")
@@ -85,6 +86,7 @@ func main() {
CachePath: *confCachePath, CachePath: *confCachePath,
CoverCachePath: coverCachePath, CoverCachePath: coverCachePath,
ProxyPrefix: *confProxyPrefix, ProxyPrefix: *confProxyPrefix,
GenreSplit: *confGenreSplit,
}) })
var g run.Group var g run.Group

View File

@@ -82,7 +82,7 @@ func (c *Controller) ServeGetAlbum(r *http.Request) *spec.Response {
Select("albums.*, count(tracks.id) child_count, sum(tracks.length) duration"). Select("albums.*, count(tracks.id) child_count, sum(tracks.length) duration").
Joins("LEFT JOIN tracks ON tracks.album_id=albums.id"). Joins("LEFT JOIN tracks ON tracks.album_id=albums.id").
Preload("TagArtist"). Preload("TagArtist").
Preload("TagGenre"). Preload("Genres").
Preload("Tracks", func(db *gorm.DB) *gorm.DB { Preload("Tracks", func(db *gorm.DB) *gorm.DB {
return db.Order("tracks.tag_disc_number, tracks.tag_track_number") return db.Order("tracks.tag_disc_number, tracks.tag_track_number")
}). }).
@@ -123,8 +123,9 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
params.GetOrInt("toYear", 2200)) params.GetOrInt("toYear", 2200))
q = q.Order("tag_year") q = q.Order("tag_year")
case "byGenre": case "byGenre":
q = q.Joins("JOIN genres ON albums.tag_genre_id=genres.id AND genres.name=?", genre, _ := params.Get("genre")
params.GetOr("genre", "Unknown Genre")) q = q.Joins("JOIN album_genres ON album_genres.album_id=albums.id")
q = q.Joins("JOIN genres ON genres.id=album_genres.genre_id AND genres.name=?", genre)
case "frequent": case "frequent":
user := r.Context().Value(CtxUser).(*db.User) user := r.Context().Value(CtxUser).(*db.User)
q = q.Joins("JOIN plays ON albums.id=plays.album_id AND plays.user_id=?", q = q.Joins("JOIN plays ON albums.id=plays.album_id AND plays.user_id=?",
@@ -291,8 +292,8 @@ func (c *Controller) ServeGetGenres(r *http.Request) *spec.Response {
var genres []*db.Genre var genres []*db.Genre
c.DB. c.DB.
Select(`*, Select(`*,
(SELECT count(id) FROM albums WHERE tag_genre_id=genres.id) album_count, (SELECT count(1) FROM album_genres WHERE genre_id=genres.id) album_count,
(SELECT count(id) FROM tracks WHERE tag_genre_id=genres.id) track_count`). (SELECT count(1) FROM track_genres WHERE genre_id=genres.id) track_count`).
Group("genres.id"). Group("genres.id").
Find(&genres) Find(&genres)
sub := spec.NewResponse() sub := spec.NewResponse()
@@ -316,7 +317,8 @@ func (c *Controller) ServeGetSongsByGenre(r *http.Request) *spec.Response {
var tracks []*db.Track var tracks []*db.Track
c.DB. c.DB.
Joins("JOIN albums ON tracks.album_id=albums.id"). Joins("JOIN albums ON tracks.album_id=albums.id").
Joins("JOIN genres ON tracks.tag_genre_id=genres.id AND genres.name=?", genre). Joins("JOIN track_genres ON track_genres.track_id=tracks.id").
Joins("JOIN genres ON track_genres.genre_id=genres.id AND genres.name=?", genre).
Preload("Album"). Preload("Album").
Offset(params.GetOrInt("offset", 0)). Offset(params.GetOrInt("offset", 0)).
Limit(params.GetOrInt("count", 10)). Limit(params.GetOrInt("count", 10)).

View File

@@ -197,9 +197,9 @@ func (c *Controller) ServeGetRandomSongs(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
var tracks []*db.Track var tracks []*db.Track
q := c.DB.DB. q := c.DB.DB.
Joins("JOIN albums ON tracks.album_id=albums.id").
Limit(params.GetOrInt("size", 10)). Limit(params.GetOrInt("size", 10)).
Preload("Album"). Preload("Album").
Joins("JOIN albums ON tracks.album_id=albums.id").
Order(gorm.Expr("random()")) Order(gorm.Expr("random()"))
if year, err := params.GetInt("fromYear"); err == nil { if year, err := params.GetInt("fromYear"); err == nil {
q = q.Where("albums.tag_year >= ?", year) q = q.Where("albums.tag_year >= ?", year)
@@ -208,10 +208,8 @@ func (c *Controller) ServeGetRandomSongs(r *http.Request) *spec.Response {
q = q.Where("albums.tag_year <= ?", year) q = q.Where("albums.tag_year <= ?", year)
} }
if genre, err := params.Get("genre"); err == nil { if genre, err := params.Get("genre"); err == nil {
q = q.Joins( q = q.Joins("JOIN track_genres ON track_genres.track_id=tracks.id")
"JOIN genres ON tracks.tag_genre_id=genres.id AND genres.name=?", q = q.Joins("JOIN genres ON genres.id=track_genres.genre_id AND genres.name=?", genre)
genre,
)
} }
q.Find(&tracks) q.Find(&tracks)
sub := spec.NewResponse() sub := spec.NewResponse()

View File

@@ -2,6 +2,7 @@ package spec
import ( import (
"path" "path"
"strings"
"go.senan.xyz/gonic/server/db" "go.senan.xyz/gonic/server/db"
) )
@@ -13,11 +14,9 @@ func NewAlbumByTags(a *db.Album, artist *db.Artist) *Album {
Name: a.TagTitle, Name: a.TagTitle,
Year: a.TagYear, Year: a.TagYear,
TrackCount: a.ChildCount, TrackCount: a.ChildCount,
Genre: strings.Join(a.GenreStrings(), ", "),
Duration: a.Duration, Duration: a.Duration,
} }
if a.TagGenre != nil {
ret.Genre = a.TagGenre.Name
}
if a.Cover != "" { if a.Cover != "" {
ret.CoverID = a.SID() ret.CoverID = a.SID()
} }
@@ -47,6 +46,7 @@ func NewTrackByTags(t *db.Track, album *db.Album) *TrackChild {
), ),
Album: album.TagTitle, Album: album.TagTitle,
AlbumID: album.SID(), AlbumID: album.SID(),
Genre: strings.Join(t.GenreStrings(), ", "),
Duration: t.Length, Duration: t.Length,
Bitrate: t.Bitrate, Bitrate: t.Bitrate,
Type: "music", Type: "music",

View File

@@ -42,12 +42,16 @@ func New(in string) (ID, error) {
if err != nil { if err != nil {
return ID{}, fmt.Errorf("%q: %w", partValue, ErrNotAnInt) return ID{}, fmt.Errorf("%q: %w", partValue, ErrNotAnInt)
} }
for _, acc := range []IDT{Artist, Album, Track} { switch IDT(partType) {
if partType == string(acc) { case Artist:
return ID{Type: acc, Value: val}, nil return ID{Type: Artist, Value: val}, nil
} case Album:
return ID{Type: Album, Value: val}, nil
case Track:
return ID{Type: Track, Value: val}, nil
default:
return ID{}, fmt.Errorf("%q: %w", partType, ErrBadPrefix)
} }
return ID{}, fmt.Errorf("%q: %w", partType, ErrBadPrefix)
} }
func (i ID) String() string { func (i ID) String() string {

View File

@@ -76,6 +76,7 @@ func New(path string) (*DB, error) {
migrateAddGenre(), migrateAddGenre(),
migrateUpdateTranscodePrefIDX(), migrateUpdateTranscodePrefIDX(),
migrateAddAlbumIDX(), migrateAddAlbumIDX(),
migrateMultiGenre(),
)) ))
if err = migr.Migrate(); err != nil { if err = migr.Migrate(); err != nil {
return nil, fmt.Errorf("migrating to latest version: %w", err) return nil, fmt.Errorf("migrating to latest version: %w", err)

View File

@@ -61,14 +61,14 @@ func migrateMergePlaylist() gormigrate.Migration {
return nil return nil
} }
return tx.Exec(` return tx.Exec(`
UPDATE playlists UPDATE playlists
SET items=( SELECT group_concat(track_id) FROM ( SET items=( SELECT group_concat(track_id) FROM (
SELECT track_id SELECT track_id
FROM playlist_items FROM playlist_items
WHERE playlist_items.playlist_id=playlists.id WHERE playlist_items.playlist_id=playlists.id
ORDER BY created_at ORDER BY created_at
) ); ) );
DROP TABLE playlist_items;`, DROP TABLE playlist_items;`,
). ).
Error Error
}, },
@@ -117,8 +117,8 @@ func migrateUpdateTranscodePrefIDX() gormigrate.Migration {
return nil return nil
} }
step := tx.Exec(` step := tx.Exec(`
ALTER TABLE transcode_preferences RENAME TO transcode_preferences_orig; ALTER TABLE transcode_preferences RENAME TO transcode_preferences_orig;
`) `)
if err := step.Error; err != nil { if err := step.Error; err != nil {
return fmt.Errorf("step rename: %w", err) return fmt.Errorf("step rename: %w", err)
} }
@@ -129,11 +129,11 @@ func migrateUpdateTranscodePrefIDX() gormigrate.Migration {
return fmt.Errorf("step create: %w", err) return fmt.Errorf("step create: %w", err)
} }
step = tx.Exec(` step = tx.Exec(`
INSERT INTO transcode_preferences (user_id, client, profile) INSERT INTO transcode_preferences (user_id, client, profile)
SELECT user_id, client, profile SELECT user_id, client, profile
FROM transcode_preferences_orig; FROM transcode_preferences_orig;
DROP TABLE transcode_preferences_orig; DROP TABLE transcode_preferences_orig;
`) `)
if err := step.Error; err != nil { if err := step.Error; err != nil {
return fmt.Errorf("step copy: %w", err) return fmt.Errorf("step copy: %w", err)
} }
@@ -153,3 +153,19 @@ func migrateAddAlbumIDX() gormigrate.Migration {
}, },
} }
} }
func migrateMultiGenre() gormigrate.Migration {
return gormigrate.Migration{
ID: "202012151806",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(
Track{},
Album{},
Genre{},
TrackGenre{},
AlbumGenre{},
).
Error
},
}
}

View File

@@ -61,12 +61,10 @@ func (a *Artist) IndexName() string {
} }
type Genre struct { type Genre struct {
ID int `gorm:"primary_key"` ID int `gorm:"primary_key"`
Name string `gorm:"not null; unique_index"` Name string `gorm:"not null; unique_index"`
Albums []*Album `gorm:"foreignkey:TagGenreID"` AlbumCount int `sql:"-"`
AlbumCount int `sql:"-"` TrackCount int `sql:"-"`
Tracks []*Track `gorm:"foreignkey:TagGenreID"`
TrackCount int `sql:"-"`
} }
type Track struct { type Track struct {
@@ -78,18 +76,17 @@ type Track struct {
Album *Album Album *Album
AlbumID int `gorm:"not null; unique_index:idx_folder_filename" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` AlbumID int `gorm:"not null; unique_index:idx_folder_filename" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
Artist *Artist Artist *Artist
ArtistID int `gorm:"not null" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"` ArtistID int `gorm:"not null" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
Size int `gorm:"not null" sql:"default: null"` Genres []*Genre `gorm:"many2many:track_genres"`
Length int `sql:"default: null"` Size int `gorm:"not null" sql:"default: null"`
Bitrate int `sql:"default: null"` Length int `sql:"default: null"`
TagTitle string `sql:"default: null"` Bitrate int `sql:"default: null"`
TagTitleUDec string `sql:"default: null"` TagTitle string `sql:"default: null"`
TagTrackArtist string `sql:"default: null"` TagTitleUDec string `sql:"default: null"`
TagTrackNumber int `sql:"default: null"` TagTrackArtist string `sql:"default: null"`
TagDiscNumber int `sql:"default: null"` TagTrackNumber int `sql:"default: null"`
TagGenre *Genre TagDiscNumber int `sql:"default: null"`
TagGenreID int `sql:"default: null; type:int REFERENCES genres(id)"` TagBrainzID string `sql:"default: null"`
TagBrainzID string `sql:"default: null"`
} }
func (t *Track) SID() *specid.ID { func (t *Track) SID() *specid.ID {
@@ -128,6 +125,14 @@ func (t *Track) RelPath() string {
) )
} }
func (t *Track) GenreStrings() []string {
var strs []string
for _, genre := range t.Genres {
strs = append(strs, genre.Name)
}
return strs
}
type User struct { type User struct {
ID int `gorm:"primary_key"` ID int `gorm:"primary_key"`
CreatedAt time.Time CreatedAt time.Time
@@ -160,12 +165,11 @@ type Album struct {
RightPath string `gorm:"not null; unique_index:idx_left_path_right_path" sql:"default: null"` RightPath string `gorm:"not null; unique_index:idx_left_path_right_path" sql:"default: null"`
RightPathUDec string `sql:"default: null"` RightPathUDec string `sql:"default: null"`
Parent *Album Parent *Album
ParentID int `sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` ParentID int `sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
Cover string `sql:"default: null"` Genres []*Genre `gorm:"many2many:album_genres"`
Cover string `sql:"default: null"`
TagArtist *Artist TagArtist *Artist
TagArtistID int `gorm:"index" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"` TagArtistID int `gorm:"index" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
TagGenre *Genre
TagGenreID int `sql:"default: null; type:int"`
TagTitle string `sql:"default: null"` TagTitle string `sql:"default: null"`
TagTitleUDec string `sql:"default: null"` TagTitleUDec string `sql:"default: null"`
TagBrainzID string `sql:"default: null"` TagBrainzID string `sql:"default: null"`
@@ -192,6 +196,14 @@ func (a *Album) IndexRightPath() string {
return a.RightPath return a.RightPath
} }
func (a *Album) GenreStrings() []string {
var strs []string
for _, genre := range a.Genres {
strs = append(strs, genre.Name)
}
return strs
}
type Playlist struct { type Playlist struct {
ID int `gorm:"primary_key"` ID int `gorm:"primary_key"`
CreatedAt time.Time CreatedAt time.Time
@@ -243,3 +255,17 @@ type TranscodePreference struct {
Client string `gorm:"not null; unique_index:idx_user_id_client" sql:"default: null"` Client string `gorm:"not null; unique_index:idx_user_id_client" sql:"default: null"`
Profile string `gorm:"not null" sql:"default: null"` Profile string `gorm:"not null" sql:"default: null"`
} }
type TrackGenre struct {
Track *Track
TrackID int `gorm:"not null; unique_index:idx_track_id_genre_id" sql:"default: null; type:int REFERENCES tracks(id) ON DELETE CASCADE"`
Genre *Genre
GenreID int `gorm:"not null; unique_index:idx_track_id_genre_id" sql:"default: null; type:int REFERENCES genres(id) ON DELETE CASCADE"`
}
type AlbumGenre struct {
Album *Album
AlbumID int `gorm:"not null; unique_index:idx_album_id_genre_id" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
Genre *Genre
GenreID int `gorm:"not null; unique_index:idx_album_id_genre_id" sql:"default: null; type:int REFERENCES genres(id) ON DELETE CASCADE"`
}

View File

@@ -58,9 +58,10 @@ func SetScanning() func() {
} }
type Scanner struct { type Scanner struct {
db *db.DB db *db.DB
musicPath string musicPath string
isFull bool isFull bool
genreSplit string
// these two are for the transaction we do for every folder. // these two are for the transaction we do for every folder.
// the boolean is there so we dont begin or commit multiple // the boolean is there so we dont begin or commit multiple
// times in the handle folder or post children callback // times in the handle folder or post children callback
@@ -78,10 +79,11 @@ type Scanner struct {
seenTracksNew int // n tracks not seen before seenTracksNew int // n tracks not seen before
} }
func New(musicPath string, db *db.DB) *Scanner { func New(musicPath string, db *db.DB, genreSplit string) *Scanner {
return &Scanner{ return &Scanner{
db: db, db: db,
musicPath: musicPath, musicPath: musicPath,
genreSplit: genreSplit,
} }
} }
@@ -368,6 +370,7 @@ func (s *Scanner) handleTrack(it *item) error {
s.trTx = s.db.Begin() s.trTx = s.db.Begin()
s.trTxOpen = true s.trTxOpen = true
} }
// ** begin set track basics // ** begin set track basics
track := &db.Track{} track := &db.Track{}
defer func() { defer func() {
@@ -404,16 +407,9 @@ func (s *Scanner) handleTrack(it *item) error {
track.TagBrainzID = trTags.BrainzID() track.TagBrainzID = trTags.BrainzID()
track.Length = trTags.Length() // these two should be calculated track.Length = trTags.Length() // these two should be calculated
track.Bitrate = trTags.Bitrate() // ...from the file instead of tags track.Bitrate = trTags.Bitrate() // ...from the file instead of tags
// ** begin set album artist basics // ** begin set album artist basics
artistName := func() string { artistName := firstTag("Unknown Artist", trTags.AlbumArtist, trTags.Artist)
if r := trTags.AlbumArtist(); r != "" {
return r
}
if r := trTags.Artist(); r != "" {
return r
}
return "Unknown Artist"
}()
artist := &db.Artist{} artist := &db.Artist{}
err = s.trTx. err = s.trTx.
Select("id"). Select("id").
@@ -428,43 +424,66 @@ func (s *Scanner) handleTrack(it *item) error {
} }
} }
track.ArtistID = artist.ID track.ArtistID = artist.ID
// ** begin set genre // ** begin set genre
genreName := func() string { genreTag := firstTag("Unknown Genre", trTags.Genre)
if r := trTags.Genre(); r != "" { genres := strings.Split(genreTag, s.genreSplit)
return r genreIDs := []int{}
} for _, genreName := range genres {
return "Unknown Genre" // TODO insert or ignore
}() genre := &db.Genre{}
genre := &db.Genre{} err = s.trTx.
err = s.trTx. Select("id").
Select("id"). Where("name=?", genreName).
Where("name=?", genreName). First(genre).
First(genre). Error
Error if gorm.IsRecordNotFoundError(err) {
if gorm.IsRecordNotFoundError(err) { genre.Name = genreName
genre.Name = genreName if err := s.trTx.Save(genre).Error; err != nil {
if err := s.trTx.Save(genre).Error; err != nil { return fmt.Errorf("writing genres table: %w", err)
return fmt.Errorf("writing genres table: %w", err) }
} }
genreIDs = append(genreIDs, genre.ID)
} }
track.TagGenreID = genre.ID
// ** begin save the track // ** begin save the track
if err := s.trTx.Save(track).Error; err != nil { if err := s.trTx.Save(track).Error; err != nil {
return fmt.Errorf("writing track table: %w", err) return fmt.Errorf("writing track table: %w", err)
} }
for _, genreID := range genreIDs {
trackGenre := &db.TrackGenre{TrackID: track.ID, GenreID: genreID}
if err := s.trTx.Save(trackGenre).Error; err != nil {
return fmt.Errorf("writing track table: %w", err)
}
}
s.seenTracksNew++ s.seenTracksNew++
// ** begin set album if this is the first track in the folder // ** begin set album if this is the first track in the folder
folder := s.curFolders.Peek() folder := s.curFolders.Peek()
if !folder.ReceivedPaths || folder.ReceivedTags { if !folder.ReceivedPaths || folder.ReceivedTags {
// the folder hasn't been modified or already has it's tags // the folder hasn't been modified or already has it's tags
return nil return nil
} }
for _, genreID := range genreIDs {
albumGenre := &db.AlbumGenre{AlbumID: folder.ID, GenreID: genreID}
if err := s.trTx.Save(albumGenre).Error; err != nil {
return fmt.Errorf("writing album table: %w", err)
}
}
folder.TagTitle = trTags.Album() folder.TagTitle = trTags.Album()
folder.TagTitleUDec = decoded(trTags.Album()) folder.TagTitleUDec = decoded(trTags.Album())
folder.TagBrainzID = trTags.AlbumBrainzID() folder.TagBrainzID = trTags.AlbumBrainzID()
folder.TagYear = trTags.Year() folder.TagYear = trTags.Year()
folder.TagArtistID = artist.ID folder.TagArtistID = artist.ID
folder.TagGenreID = genre.ID
folder.ReceivedTags = true folder.ReceivedTags = true
return nil return nil
} }
func firstTag(fallback string, tags ...func() string) string {
for _, f := range tags {
if tag := f(); tag != "" {
return tag
}
}
return fallback
}

View File

@@ -26,6 +26,7 @@ type Options struct {
CachePath string CachePath string
CoverCachePath string CoverCachePath string
ProxyPrefix string ProxyPrefix string
GenreSplit string
} }
type Server struct { type Server struct {
@@ -40,7 +41,7 @@ func New(opts Options) *Server {
opts.MusicPath = filepath.Clean(opts.MusicPath) opts.MusicPath = filepath.Clean(opts.MusicPath)
opts.CachePath = filepath.Clean(opts.CachePath) opts.CachePath = filepath.Clean(opts.CachePath)
// ** begin controllers // ** begin controllers
scanner := scanner.New(opts.MusicPath, opts.DB) scanner := scanner.New(opts.MusicPath, opts.DB, opts.GenreSplit)
jukebox := jukebox.New(opts.MusicPath) jukebox := jukebox.New(opts.MusicPath)
// the base controller, it's fields/middlewares are embedded/used by the // the base controller, it's fields/middlewares are embedded/used by the
// other two admin ui and subsonic controllers // other two admin ui and subsonic controllers