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)")
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)")
confGenreSplit := set.String("genre-split", "\n", "character or string to split genre tag data on (optional)")
confShowVersion := set.Bool("version", false, "show gonic version")
_ = set.String("config-path", "", "path to config (optional)")
@@ -85,6 +86,7 @@ func main() {
CachePath: *confCachePath,
CoverCachePath: coverCachePath,
ProxyPrefix: *confProxyPrefix,
GenreSplit: *confGenreSplit,
})
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").
Joins("LEFT JOIN tracks ON tracks.album_id=albums.id").
Preload("TagArtist").
Preload("TagGenre").
Preload("Genres").
Preload("Tracks", func(db *gorm.DB) *gorm.DB {
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))
q = q.Order("tag_year")
case "byGenre":
q = q.Joins("JOIN genres ON albums.tag_genre_id=genres.id AND genres.name=?",
params.GetOr("genre", "Unknown Genre"))
genre, _ := params.Get("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":
user := r.Context().Value(CtxUser).(*db.User)
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
c.DB.
Select(`*,
(SELECT count(id) FROM albums WHERE tag_genre_id=genres.id) album_count,
(SELECT count(id) FROM tracks WHERE tag_genre_id=genres.id) track_count`).
(SELECT count(1) FROM album_genres WHERE genre_id=genres.id) album_count,
(SELECT count(1) FROM track_genres WHERE genre_id=genres.id) track_count`).
Group("genres.id").
Find(&genres)
sub := spec.NewResponse()
@@ -316,7 +317,8 @@ func (c *Controller) ServeGetSongsByGenre(r *http.Request) *spec.Response {
var tracks []*db.Track
c.DB.
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").
Offset(params.GetOrInt("offset", 0)).
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)
var tracks []*db.Track
q := c.DB.DB.
Joins("JOIN albums ON tracks.album_id=albums.id").
Limit(params.GetOrInt("size", 10)).
Preload("Album").
Joins("JOIN albums ON tracks.album_id=albums.id").
Order(gorm.Expr("random()"))
if year, err := params.GetInt("fromYear"); err == nil {
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)
}
if genre, err := params.Get("genre"); err == nil {
q = q.Joins(
"JOIN genres ON tracks.tag_genre_id=genres.id AND genres.name=?",
genre,
)
q = q.Joins("JOIN track_genres ON track_genres.track_id=tracks.id")
q = q.Joins("JOIN genres ON genres.id=track_genres.genre_id AND genres.name=?", genre)
}
q.Find(&tracks)
sub := spec.NewResponse()

View File

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

View File

@@ -42,13 +42,17 @@ func New(in string) (ID, error) {
if err != nil {
return ID{}, fmt.Errorf("%q: %w", partValue, ErrNotAnInt)
}
for _, acc := range []IDT{Artist, Album, Track} {
if partType == string(acc) {
return ID{Type: acc, Value: val}, nil
}
}
switch IDT(partType) {
case Artist:
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)
}
}
func (i ID) String() string {
if i.Value == 0 {

View File

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

View File

@@ -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

@@ -63,9 +63,7 @@ func (a *Artist) IndexName() string {
type Genre struct {
ID int `gorm:"primary_key"`
Name string `gorm:"not null; unique_index"`
Albums []*Album `gorm:"foreignkey:TagGenreID"`
AlbumCount int `sql:"-"`
Tracks []*Track `gorm:"foreignkey:TagGenreID"`
TrackCount int `sql:"-"`
}
@@ -79,6 +77,7 @@ type Track struct {
AlbumID int `gorm:"not null; unique_index:idx_folder_filename" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
Artist *Artist
ArtistID int `gorm:"not null" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
Genres []*Genre `gorm:"many2many:track_genres"`
Size int `gorm:"not null" sql:"default: null"`
Length int `sql:"default: null"`
Bitrate int `sql:"default: null"`
@@ -87,8 +86,6 @@ type Track struct {
TagTrackArtist string `sql:"default: null"`
TagTrackNumber int `sql:"default: null"`
TagDiscNumber int `sql:"default: null"`
TagGenre *Genre
TagGenreID int `sql:"default: null; type:int REFERENCES genres(id)"`
TagBrainzID string `sql:"default: null"`
}
@@ -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 {
ID int `gorm:"primary_key"`
CreatedAt time.Time
@@ -161,11 +166,10 @@ type Album struct {
RightPathUDec string `sql:"default: null"`
Parent *Album
ParentID int `sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
Genres []*Genre `gorm:"many2many:album_genres"`
Cover string `sql:"default: null"`
TagArtist *Artist
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"`
TagTitleUDec string `sql:"default: null"`
TagBrainzID string `sql:"default: null"`
@@ -192,6 +196,14 @@ func (a *Album) IndexRightPath() string {
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 {
ID int `gorm:"primary_key"`
CreatedAt time.Time
@@ -243,3 +255,17 @@ type TranscodePreference struct {
Client string `gorm:"not null; unique_index:idx_user_id_client" 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

@@ -61,6 +61,7 @@ type Scanner struct {
db *db.DB
musicPath string
isFull bool
genreSplit string
// these two are for the transaction we do for every folder.
// the boolean is there so we dont begin or commit multiple
// times in the handle folder or post children callback
@@ -78,10 +79,11 @@ type Scanner struct {
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{
db: db,
musicPath: musicPath,
genreSplit: genreSplit,
}
}
@@ -368,6 +370,7 @@ func (s *Scanner) handleTrack(it *item) error {
s.trTx = s.db.Begin()
s.trTxOpen = true
}
// ** begin set track basics
track := &db.Track{}
defer func() {
@@ -404,16 +407,9 @@ func (s *Scanner) handleTrack(it *item) error {
track.TagBrainzID = trTags.BrainzID()
track.Length = trTags.Length() // these two should be calculated
track.Bitrate = trTags.Bitrate() // ...from the file instead of tags
// ** begin set album artist basics
artistName := func() string {
if r := trTags.AlbumArtist(); r != "" {
return r
}
if r := trTags.Artist(); r != "" {
return r
}
return "Unknown Artist"
}()
artistName := firstTag("Unknown Artist", trTags.AlbumArtist, trTags.Artist)
artist := &db.Artist{}
err = s.trTx.
Select("id").
@@ -428,13 +424,13 @@ func (s *Scanner) handleTrack(it *item) error {
}
}
track.ArtistID = artist.ID
// ** begin set genre
genreName := func() string {
if r := trTags.Genre(); r != "" {
return r
}
return "Unknown Genre"
}()
genreTag := firstTag("Unknown Genre", trTags.Genre)
genres := strings.Split(genreTag, s.genreSplit)
genreIDs := []int{}
for _, genreName := range genres {
// TODO insert or ignore
genre := &db.Genre{}
err = s.trTx.
Select("id").
@@ -447,24 +443,47 @@ func (s *Scanner) handleTrack(it *item) error {
return fmt.Errorf("writing genres table: %w", err)
}
}
track.TagGenreID = genre.ID
genreIDs = append(genreIDs, genre.ID)
}
// ** begin save the track
if err := s.trTx.Save(track).Error; err != nil {
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++
// ** begin set album if this is the first track in the folder
folder := s.curFolders.Peek()
if !folder.ReceivedPaths || folder.ReceivedTags {
// the folder hasn't been modified or already has it's tags
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.TagTitleUDec = decoded(trTags.Album())
folder.TagBrainzID = trTags.AlbumBrainzID()
folder.TagYear = trTags.Year()
folder.TagArtistID = artist.ID
folder.TagGenreID = genre.ID
folder.ReceivedTags = true
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
CoverCachePath string
ProxyPrefix string
GenreSplit string
}
type Server struct {
@@ -40,7 +41,7 @@ func New(opts Options) *Server {
opts.MusicPath = filepath.Clean(opts.MusicPath)
opts.CachePath = filepath.Clean(opts.CachePath)
// ** begin controllers
scanner := scanner.New(opts.MusicPath, opts.DB)
scanner := scanner.New(opts.MusicPath, opts.DB, opts.GenreSplit)
jukebox := jukebox.New(opts.MusicPath)
// the base controller, it's fields/middlewares are embedded/used by the
// other two admin ui and subsonic controllers