Merge branch 'develop'
This commit is contained in:
1
db/db.go
1
db/db.go
@@ -41,6 +41,7 @@ func New(path string) (*DB, error) {
|
||||
&migrationCreateInitUser,
|
||||
&migrationMergePlaylist,
|
||||
&migrationCreateTranscode,
|
||||
&migrationAddGenre,
|
||||
})
|
||||
if err = migr.Migrate(); err != nil {
|
||||
return nil, errors.Wrap(err, "migrating to latest version")
|
||||
|
||||
@@ -79,3 +79,15 @@ var migrationCreateTranscode = gormigrate.Migration{
|
||||
Error
|
||||
},
|
||||
}
|
||||
|
||||
var migrationAddGenre = gormigrate.Migration{
|
||||
ID: "202003121330",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
Genre{},
|
||||
Album{},
|
||||
Track{},
|
||||
).
|
||||
Error
|
||||
},
|
||||
}
|
||||
|
||||
15
db/model.go
15
db/model.go
@@ -49,6 +49,15 @@ func (a *Artist) IndexName() string {
|
||||
return a.Name
|
||||
}
|
||||
|
||||
type Genre struct {
|
||||
ID int `gorm:"primary_ket"`
|
||||
Name string `gorm:"not null; unique_index"`
|
||||
Albums []*Album `gorm:"foreignkey:TagGenreID"`
|
||||
AlbumCount int `sql:"-"`
|
||||
Tracks []*Track `gorm:"foreignkey:TagGenreID"`
|
||||
TrackCount int `sql:"-"`
|
||||
}
|
||||
|
||||
type Track struct {
|
||||
ID int `gorm:"primary_key"`
|
||||
CreatedAt time.Time
|
||||
@@ -67,6 +76,8 @@ 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) ON DELETE CASCADE"`
|
||||
TagBrainzID string `sql:"default: null"`
|
||||
}
|
||||
|
||||
@@ -129,7 +140,9 @@ type Album struct {
|
||||
ParentID int `sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
|
||||
Cover string `sql:"default: null"`
|
||||
TagArtist *Artist
|
||||
TagArtistID int `sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
|
||||
TagArtistID int `sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
|
||||
TagGenre *Genre
|
||||
TagGenreID int `sql:"default: null; type:int REFERENCES genres(id) ON DELETE CASCADE"`
|
||||
TagTitle string `sql:"default: null"`
|
||||
TagTitleUDec string `sql:"default: null"`
|
||||
TagBrainzID string `sql:"default: null"`
|
||||
|
||||
@@ -362,6 +362,27 @@ func (s *Scanner) handleTrack(it *item) error {
|
||||
s.trTx.Save(artist)
|
||||
}
|
||||
track.ArtistID = artist.ID
|
||||
//
|
||||
// set genre
|
||||
genreName := func() string {
|
||||
if r := trTags.Genre(); r != "" {
|
||||
return r
|
||||
}
|
||||
return "Unknown Genre"
|
||||
}()
|
||||
genre := &db.Genre{}
|
||||
err = s.trTx.
|
||||
Select("id").
|
||||
Where("name=?", genreName).
|
||||
First(genre).
|
||||
Error
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
genre.Name = genreName
|
||||
s.trTx.Save(genre)
|
||||
}
|
||||
track.TagGenreID = genre.ID
|
||||
//
|
||||
// save the track
|
||||
s.trTx.Save(track)
|
||||
s.seenTracks[track.ID] = struct{}{}
|
||||
s.seenTracksNew++
|
||||
@@ -377,6 +398,7 @@ func (s *Scanner) handleTrack(it *item) error {
|
||||
folder.TagBrainzID = trTags.AlbumBrainzID()
|
||||
folder.TagYear = trTags.Year()
|
||||
folder.TagArtistID = artist.ID
|
||||
folder.TagGenreID = genre.ID
|
||||
folder.ReceivedTags = true
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ func (t *Tags) Artist() string { return t.firstTag("artist") }
|
||||
func (t *Tags) Album() string { return t.firstTag("album") }
|
||||
func (t *Tags) AlbumArtist() string { return t.firstTag("albumartist", "album artist") }
|
||||
func (t *Tags) AlbumBrainzID() string { return t.firstTag("musicbrainz_albumid") }
|
||||
func (t *Tags) Genre() string { return t.firstTag("genre") }
|
||||
func (t *Tags) Year() int { return intSep(t.firstTag("date", "year"), "-") } // eg. 2019-6-11
|
||||
func (t *Tags) TrackNumber() int { return intSep(t.firstTag("tracknumber"), "/") } // eg. 5/12
|
||||
func (t *Tags) DiscNumber() int { return intSep(t.firstTag("discnumber"), "/") } // eg. 1/2
|
||||
|
||||
@@ -115,6 +115,9 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
|
||||
params.GetIntOr("fromYear", 1800),
|
||||
params.GetIntOr("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"))
|
||||
case "frequent":
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
q = q.Joins("JOIN plays ON albums.id=plays.album_id AND plays.user_id=?",
|
||||
@@ -275,3 +278,51 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response {
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
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`).
|
||||
Group("genres.id").
|
||||
Find(&genres)
|
||||
|
||||
sub := spec.NewResponse()
|
||||
sub.Genres = &spec.Genres{
|
||||
List: make([]*spec.Genre, len(genres)),
|
||||
}
|
||||
for i, genre := range genres {
|
||||
sub.Genres.List[i] = spec.NewGenre(genre)
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
func (c *Controller) ServeGetSongsByGenre(r *http.Request) *spec.Response {
|
||||
params := r.Context().Value(CtxParams).(params.Params)
|
||||
genre := params.Get("genre")
|
||||
if genre == "" {
|
||||
return spec.NewError(10, "please provide an `genre` parameter")
|
||||
}
|
||||
|
||||
// TODO: add musicFolderId parameter:
|
||||
// (Since 1.12.0) Only return albums in the music folder with the given ID.
|
||||
|
||||
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).
|
||||
Preload("Album").
|
||||
Offset(params.GetIntOr("offset", 0)).
|
||||
Limit(params.GetIntOr("count", 10)).
|
||||
Find(&tracks)
|
||||
|
||||
sub := spec.NewResponse()
|
||||
sub.TracksByGenre = &spec.TracksByGenre{
|
||||
List: make([]*spec.TrackChild, len(tracks)),
|
||||
}
|
||||
for i, track := range tracks {
|
||||
sub.TracksByGenre.List[i] = spec.NewTrackByTags(track, track.Album)
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
@@ -286,18 +286,28 @@ func (c *Controller) ServeGetSong(r *http.Request) *spec.Response {
|
||||
|
||||
func (c *Controller) ServeGetRandomSongs(r *http.Request) *spec.Response {
|
||||
params := r.Context().Value(CtxParams).(params.Params)
|
||||
// TODO: add genre restraint here
|
||||
var tracks []*db.Track
|
||||
c.DB.DB.
|
||||
Limit(params.GetIntOr("size", 10)).
|
||||
Where(
|
||||
"albums.tag_year BETWEEN ? AND ?",
|
||||
params.GetIntOr("fromYear", 1800),
|
||||
params.GetIntOr("toYear", 2200)).
|
||||
|
||||
q := c.DB.DB.
|
||||
Joins("JOIN albums ON tracks.album_id=albums.id").
|
||||
Limit(params.GetIntOr("size", 10)).
|
||||
Preload("Album").
|
||||
Order(gorm.Expr("random()")).
|
||||
Find(&tracks)
|
||||
Order(gorm.Expr("random()"))
|
||||
|
||||
if year, err := params.GetInt("fromYear"); err == nil {
|
||||
q = q.Where("albums.tag_year >= ?", year)
|
||||
}
|
||||
if year, err := params.GetInt("toYear"); err == nil {
|
||||
q = q.Where("albums.tag_year <= ?", year)
|
||||
}
|
||||
if genre := params.Get("genre"); genre != "" {
|
||||
q = q.Joins(
|
||||
"JOIN genres ON tracks.tag_genre_id=genres.id AND genres.name=?",
|
||||
genre,
|
||||
)
|
||||
}
|
||||
q.Find(&tracks)
|
||||
|
||||
sub := spec.NewResponse()
|
||||
sub.RandomTracks = &spec.RandomTracks{}
|
||||
sub.RandomTracks.List = make([]*spec.TrackChild, len(tracks))
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
package ctrlsubsonic
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
|
||||
)
|
||||
|
||||
// NOTE: when these are implemented, they should be moved to their
|
||||
// respective _by_folder or _by_tag file
|
||||
|
||||
func (c *Controller) ServeGetGenres(r *http.Request) *spec.Response {
|
||||
sub := spec.NewResponse()
|
||||
sub.Genres = &spec.Genres{}
|
||||
sub.Genres.List = []*spec.Genre{}
|
||||
return sub
|
||||
}
|
||||
|
||||
@@ -72,3 +72,11 @@ func NewArtistByTags(a *db.Artist) *Artist {
|
||||
AlbumCount: a.AlbumCount,
|
||||
}
|
||||
}
|
||||
|
||||
func NewGenre(g *db.Genre) *Genre {
|
||||
return &Genre{
|
||||
Name: g.Name,
|
||||
AlbumCount: g.AlbumCount,
|
||||
SongCount: g.TrackCount,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ type Response struct {
|
||||
Artist *Artist `xml:"artist" json:"artist,omitempty"`
|
||||
Directory *Directory `xml:"directory" json:"directory,omitempty"`
|
||||
RandomTracks *RandomTracks `xml:"randomSongs" json:"randomSongs,omitempty"`
|
||||
TracksByGenre *TracksByGenre `xml:"songsByGenre" json:"songsByGenre,omitempty"`
|
||||
MusicFolders *MusicFolders `xml:"musicFolders" json:"musicFolders,omitempty"`
|
||||
ScanStatus *ScanStatus `xml:"scanStatus" json:"scanStatus,omitempty"`
|
||||
Licence *Licence `xml:"license" json:"license,omitempty"`
|
||||
@@ -109,6 +110,10 @@ type RandomTracks struct {
|
||||
List []*TrackChild `xml:"song" json:"song"`
|
||||
}
|
||||
|
||||
type TracksByGenre struct {
|
||||
List []*TrackChild `xml:"song" json:"song"`
|
||||
}
|
||||
|
||||
type TrackChild struct {
|
||||
Album string `xml:"album,attr,omitempty" json:"album,omitempty"`
|
||||
AlbumID int `xml:"albumId,attr,omitempty" json:"albumId,omitempty,string"`
|
||||
@@ -250,8 +255,9 @@ type Genres struct {
|
||||
}
|
||||
|
||||
type Genre struct {
|
||||
SongCount string `xml:"songCount,attr"`
|
||||
AlbumCount string `xml:"albumCount,attr"`
|
||||
Name string `xml:",chardata",json:"value"`
|
||||
SongCount int `xml:"songCount,attr,omitempty" json:"songCount,omitempty"`
|
||||
AlbumCount int `xml:"albumCount,attr,omitempty" json:"albumCount,omitempty"`
|
||||
}
|
||||
|
||||
type PlayQueue struct {
|
||||
|
||||
@@ -150,6 +150,7 @@ func setupSubsonic(r *mux.Router, ctrl *ctrlsubsonic.Controller) {
|
||||
r.Handle("/getPlayQueue{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetPlayQueue))
|
||||
r.Handle("/getSong{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetSong))
|
||||
r.Handle("/getRandomSongs{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetRandomSongs))
|
||||
r.Handle("/getSongsByGenre{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetSongsByGenre))
|
||||
// ** begin raw
|
||||
r.Handle("/download{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeDownload))
|
||||
r.Handle("/getCoverArt{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeGetCoverArt))
|
||||
@@ -166,8 +167,8 @@ func setupSubsonic(r *mux.Router, ctrl *ctrlsubsonic.Controller) {
|
||||
r.Handle("/getMusicDirectory{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetMusicDirectory))
|
||||
r.Handle("/getAlbumList{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetAlbumList))
|
||||
r.Handle("/search2{_:(?:\\.view)?}", ctrl.H(ctrl.ServeSearchTwo))
|
||||
// ** begin unimplemented
|
||||
r.Handle("/getGenres{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetGenres))
|
||||
// ** begin unimplemented
|
||||
// middlewares should be run for not found handler
|
||||
// https://github.com/gorilla/mux/issues/416
|
||||
notFoundHandler := ctrl.H(ctrl.ServeNotFound)
|
||||
|
||||
Reference in New Issue
Block a user