Implements library scanning endpoints. Also:

- Bumped Subsonic API version to 1.15:
- Better User/Users Subsonic endpoint implementations, not final though
This commit is contained in:
Deluan 2020-10-27 18:19:56 -04:00
parent 9b756faef5
commit d9f7a154cf
17 changed files with 196 additions and 37 deletions

View File

@ -78,7 +78,7 @@ func startServer() (func() error, func(err error)) {
func startScanner() (func() error, func(err error)) {
interval := conf.Server.ScanInterval
log.Info("Starting scanner", "interval", interval.String())
scanner := CreateScanner(conf.Server.MusicFolder)
scanner := GetScanner()
return func() error {
if interval != 0 {

View File

@ -3,7 +3,6 @@ package cmd
import (
"time"
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/scanner"
"github.com/spf13/cobra"
@ -37,7 +36,7 @@ func waitScanToFinish(scanner scanner.Scanner) {
}
func runScanner() {
scanner := CreateScanner(conf.Server.MusicFolder)
scanner := GetScanner()
go func() { _ = scanner.Start(0) }()
scanner.RescanAll(fullRescan)
waitScanToFinish(scanner)

View File

@ -14,6 +14,7 @@ import (
"github.com/deluan/navidrome/server/app"
"github.com/deluan/navidrome/server/subsonic"
"github.com/google/wire"
"sync"
)
// Injectors from wire_injectors.go:
@ -24,15 +25,6 @@ func CreateServer(musicFolder string) *server.Server {
return serverServer
}
func CreateScanner(musicFolder string) scanner.Scanner {
dataStore := persistence.New()
artworkCache := core.GetImageCache()
artwork := core.NewArtwork(dataStore, artworkCache)
cacheWarmer := core.NewCacheWarmer(artwork)
scannerScanner := scanner.New(dataStore, cacheWarmer)
return scannerScanner
}
func CreateAppRouter() *app.Router {
dataStore := persistence.New()
router := app.New(dataStore)
@ -51,10 +43,32 @@ func CreateSubsonicAPIRouter() *subsonic.Router {
client := core.LastFMNewClient()
spotifyClient := core.SpotifyNewClient()
externalInfo := core.NewExternalInfo(dataStore, client, spotifyClient)
router := subsonic.New(artwork, mediaStreamer, archiver, players, externalInfo, dataStore)
scanner := GetScanner()
router := subsonic.New(dataStore, artwork, mediaStreamer, archiver, players, externalInfo, scanner)
return router
}
func createScanner() scanner.Scanner {
dataStore := persistence.New()
artworkCache := core.GetImageCache()
artwork := core.NewArtwork(dataStore, artworkCache)
cacheWarmer := core.NewCacheWarmer(artwork)
scannerScanner := scanner.New(dataStore, cacheWarmer)
return scannerScanner
}
// wire_injectors.go:
var allProviders = wire.NewSet(core.Set, scanner.New, subsonic.New, app.New, persistence.New)
var allProviders = wire.NewSet(core.Set, subsonic.New, app.New, persistence.New)
var (
onceScanner sync.Once
scannerInstance scanner.Scanner
)
func GetScanner() scanner.Scanner {
onceScanner.Do(func() {
scannerInstance = createScanner()
})
return scannerInstance
}

View File

@ -14,7 +14,6 @@ import (
var allProviders = wire.NewSet(
core.Set,
scanner.New,
subsonic.New,
app.New,
persistence.New,
@ -27,16 +26,33 @@ func CreateServer(musicFolder string) *server.Server {
))
}
func CreateScanner(musicFolder string) scanner.Scanner {
panic(wire.Build(
allProviders,
))
}
func CreateAppRouter() *app.Router {
panic(wire.Build(allProviders))
}
func CreateSubsonicAPIRouter() *subsonic.Router {
panic(wire.Build(allProviders))
panic(wire.Build(
allProviders,
GetScanner,
))
}
// Scanner must be a Singleton
var (
onceScanner sync.Once
scannerInstance scanner.Scanner
)
func GetScanner() scanner.Scanner {
onceScanner.Do(func() {
scannerInstance = createScanner()
})
return scannerInstance
}
func createScanner() scanner.Scanner {
panic(wire.Build(
allProviders,
scanner.New,
))
}

View File

@ -11,36 +11,39 @@ import (
"github.com/deluan/navidrome/core"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/scanner"
"github.com/deluan/navidrome/server/subsonic/responses"
"github.com/deluan/navidrome/utils"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
)
const Version = "1.13.0"
const Version = "1.15.0"
type handler = func(http.ResponseWriter, *http.Request) (*responses.Subsonic, error)
type Router struct {
DataStore model.DataStore
Artwork core.Artwork
Streamer core.MediaStreamer
Archiver core.Archiver
Players core.Players
ExternalInfo core.ExternalInfo
DataStore model.DataStore
Scanner scanner.Scanner
mux http.Handler
}
func New(artwork core.Artwork, streamer core.MediaStreamer, archiver core.Archiver, players core.Players,
externalInfo core.ExternalInfo, ds model.DataStore) *Router {
func New(ds model.DataStore, artwork core.Artwork, streamer core.MediaStreamer, archiver core.Archiver, players core.Players,
externalInfo core.ExternalInfo, scanner scanner.Scanner) *Router {
r := &Router{
DataStore: ds,
Artwork: artwork,
Streamer: streamer,
Archiver: archiver,
Players: players,
ExternalInfo: externalInfo,
DataStore: ds,
Scanner: scanner,
}
r.mux = r.routes()
return r
@ -129,6 +132,12 @@ func (api *Router) routes() http.Handler {
r.Group(func(r chi.Router) {
c := initUsersController(api)
h(r, "getUser", c.GetUser)
h(r, "getUsers", c.GetUsers)
})
r.Group(func(r chi.Router) {
c := initLibraryScanningController(api)
h(r, "getScanStatus", c.GetScanStatus)
h(r, "startScan", c.StartScan)
})
r.Group(func(r chi.Router) {
c := initMediaRetrievalController(api)

View File

@ -0,0 +1,44 @@
package subsonic
import (
"net/http"
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/scanner"
"github.com/deluan/navidrome/server/subsonic/responses"
"github.com/deluan/navidrome/utils"
)
type LibraryScanningController struct {
scanner scanner.Scanner
}
func NewLibraryScanningController(scanner scanner.Scanner) *LibraryScanningController {
return &LibraryScanningController{scanner: scanner}
}
func (c *LibraryScanningController) GetScanStatus(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
// TODO handle multiple mediafolders
ctx := r.Context()
mediaFolder := conf.Server.MusicFolder
status, err := c.scanner.Status(mediaFolder)
if err != nil {
log.Error(ctx, "Error retrieving Scanner status", err)
return nil, newError(responses.ErrorGeneric, "Internal Error")
}
response := newResponse()
response.ScanStatus = &responses.ScanStatus{
Scanning: status.Scanning,
Count: status.Count,
LastScan: &status.LastScan,
}
return response, nil
}
func (c *LibraryScanningController) StartScan(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
fullScan := utils.ParamBool(r, "fullScan", false)
c.scanner.RescanAll(fullScan)
return c.GetScanStatus(w, r)
}

View File

@ -1 +1 @@
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","scanStatus":{"scanning":true,"count":123}}
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","scanStatus":{"scanning":true,"count":123,"lastScan":"2006-01-02T15:04:00Z"}}

View File

@ -1 +1 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><scanStatus scanning="true" count="123"></scanStatus></subsonic-response>
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><scanStatus scanning="true" count="123" lastScan="2006-01-02T15:04:00Z"></scanStatus></subsonic-response>

View File

@ -0,0 +1 @@
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","users":{"user":[{"username":"deluan","email":"navidrome@deluan.com","scrobblingEnabled":false,"adminRole":true,"settingsRole":false,"downloadRole":false,"uploadRole":false,"playlistRole":false,"coverArtRole":false,"commentRole":false,"podcastRole":false,"streamRole":false,"jukeboxRole":false,"shareRole":false,"videoConversionRole":false,"folder":[1]}]}}

View File

@ -0,0 +1 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><users><user username="deluan" email="navidrome@deluan.com" scrobblingEnabled="false" adminRole="true" settingsRole="false" downloadRole="false" uploadRole="false" playlistRole="false" coverArtRole="false" commentRole="false" podcastRole="false" streamRole="false" jukeboxRole="false" shareRole="false" videoConversionRole="false"><folder>1</folder></user></users></subsonic-response>

View File

@ -0,0 +1 @@
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","users":{"user":[{"username":"deluan","scrobblingEnabled":false,"adminRole":false,"settingsRole":false,"downloadRole":false,"uploadRole":false,"playlistRole":false,"coverArtRole":false,"commentRole":false,"podcastRole":false,"streamRole":false,"jukeboxRole":false,"shareRole":false,"videoConversionRole":false}]}}

View File

@ -0,0 +1 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><users><user username="deluan" scrobblingEnabled="false" adminRole="false" settingsRole="false" downloadRole="false" uploadRole="false" playlistRole="false" coverArtRole="false" commentRole="false" podcastRole="false" streamRole="false" jukeboxRole="false" shareRole="false" videoConversionRole="false"></user></users></subsonic-response>

View File

@ -17,6 +17,7 @@ type Subsonic struct {
Indexes *Indexes `xml:"indexes,omitempty" json:"indexes,omitempty"`
Directory *Directory `xml:"directory,omitempty" json:"directory,omitempty"`
User *User `xml:"user,omitempty" json:"user,omitempty"`
Users *Users `xml:"users,omitempty" json:"users,omitempty"`
AlbumList *AlbumList `xml:"albumList,omitempty" json:"albumList,omitempty"`
AlbumList2 *AlbumList `xml:"albumList2,omitempty" json:"albumList2,omitempty"`
Playlists *Playlists `xml:"playlists,omitempty" json:"playlists,omitempty"`
@ -270,6 +271,10 @@ type User struct {
Folder []int `xml:"folder,omitempty" json:"folder,omitempty"`
}
type Users struct {
User []User `xml:"user" json:"user"`
}
type Genre struct {
Name string `xml:",chardata" json:"value,omitempty"`
SongCount int `xml:"songCount,attr" json:"songCount"`
@ -334,6 +339,7 @@ type Bookmarks struct {
}
type ScanStatus struct {
Scanning bool `xml:"scanning,attr" json:"scanning"`
Count int64 `xml:"count,attr" json:"count"`
Scanning bool `xml:"scanning,attr" json:"scanning"`
Count int64 `xml:"count,attr" json:"count"`
LastScan *time.Time `xml:"lastScan,attr,omitempty" json:"lastScan,omitempty"`
}

View File

@ -220,6 +220,39 @@ var _ = Describe("Responses", func() {
})
})
Describe("Users", func() {
BeforeEach(func() {
u := User{Username: "deluan"}
response.Users = &Users{User: []User{u}}
})
Context("without data", func() {
It("should match .XML", func() {
Expect(xml.Marshal(response)).To(MatchSnapshot())
})
It("should match .JSON", func() {
Expect(json.Marshal(response)).To(MatchSnapshot())
})
})
Context("with data", func() {
BeforeEach(func() {
u := User{Username: "deluan"}
u.Email = "navidrome@deluan.com"
u.AdminRole = true
u.Folder = []int{1}
response.Users = &Users{User: []User{u}}
})
It("should match .XML", func() {
Expect(xml.Marshal(response)).To(MatchSnapshot())
})
It("should match .JSON", func() {
Expect(json.Marshal(response)).To(MatchSnapshot())
})
})
})
Describe("Playlists", func() {
BeforeEach(func() {
response.Playlists = &Playlists{}
@ -504,9 +537,11 @@ var _ = Describe("Responses", func() {
Context("with data", func() {
BeforeEach(func() {
t, _ := time.Parse(time.RFC822, time.RFC822)
response.ScanStatus = &ScanStatus{
Scanning: true,
Count: 123,
LastScan: &t,
}
})
It("should match .XML", func() {

View File

@ -3,6 +3,7 @@ package subsonic
import (
"net/http"
"github.com/deluan/navidrome/model/request"
"github.com/deluan/navidrome/server/subsonic/responses"
)
@ -14,15 +15,34 @@ func NewUsersController() *UsersController {
// TODO This is a placeholder. The real one has to read this info from a config file or the database
func (c *UsersController) GetUser(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
user, err := requiredParamString(r, "username")
if err != nil {
return nil, err
loggedUser, ok := request.UserFrom(r.Context())
if !ok {
return nil, newError(responses.ErrorGeneric, "Internal error")
}
response := newResponse()
response.User = &responses.User{}
response.User.Username = user
response.User.Username = loggedUser.UserName
response.User.AdminRole = loggedUser.IsAdmin
response.User.Email = loggedUser.Email
response.User.StreamRole = true
response.User.DownloadRole = true
response.User.ScrobblingEnabled = true
return response, nil
}
func (c *UsersController) GetUsers(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
loggedUser, ok := request.UserFrom(r.Context())
if !ok {
return nil, newError(responses.ErrorGeneric, "Internal error")
}
user := responses.User{}
user.Username = loggedUser.Name
user.AdminRole = loggedUser.IsAdmin
user.Email = loggedUser.Email
user.StreamRole = true
user.DownloadRole = true
user.ScrobblingEnabled = true
response := newResponse()
response.Users = &responses.Users{User: []responses.User{user}}
return response, nil
}

View File

@ -75,6 +75,12 @@ func initBookmarksController(router *Router) *BookmarksController {
return bookmarksController
}
func initLibraryScanningController(router *Router) *LibraryScanningController {
scanner := router.Scanner
libraryScanningController := NewLibraryScanningController(scanner)
return libraryScanningController
}
// wire_injectors.go:
var allProviders = wire.NewSet(
@ -87,5 +93,6 @@ var allProviders = wire.NewSet(
NewUsersController,
NewMediaRetrievalController,
NewStreamController,
NewBookmarksController, core.NewNowPlayingRepository, wire.FieldsOf(new(*Router), "Artwork", "Streamer", "Archiver", "DataStore", "ExternalInfo"),
NewBookmarksController,
NewLibraryScanningController, core.NewNowPlayingRepository, wire.FieldsOf(new(*Router), "DataStore", "Artwork", "Streamer", "Archiver", "ExternalInfo", "Scanner"),
)

View File

@ -18,8 +18,9 @@ var allProviders = wire.NewSet(
NewMediaRetrievalController,
NewStreamController,
NewBookmarksController,
NewLibraryScanningController,
core.NewNowPlayingRepository,
wire.FieldsOf(new(*Router), "Artwork", "Streamer", "Archiver", "DataStore", "ExternalInfo"),
wire.FieldsOf(new(*Router), "DataStore", "Artwork", "Streamer", "Archiver", "ExternalInfo", "Scanner"),
)
func initSystemController(router *Router) *SystemController {
@ -61,3 +62,7 @@ func initStreamController(router *Router) *StreamController {
func initBookmarksController(router *Router) *BookmarksController {
panic(wire.Build(allProviders))
}
func initLibraryScanningController(router *Router) *LibraryScanningController {
panic(wire.Build(allProviders))
}