diff --git a/cmd/root.go b/cmd/root.go
index 751c090b0..b3acd9fdc 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -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 {
diff --git a/cmd/scan.go b/cmd/scan.go
index cca15624a..93fc092b9 100644
--- a/cmd/scan.go
+++ b/cmd/scan.go
@@ -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)
diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go
index 8fb28df38..6c0e242b1 100644
--- a/cmd/wire_gen.go
+++ b/cmd/wire_gen.go
@@ -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
+}
diff --git a/cmd/wire_injectors.go b/cmd/wire_injectors.go
index 0572578cc..d161b10f8 100644
--- a/cmd/wire_injectors.go
+++ b/cmd/wire_injectors.go
@@ -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,
+ ))
}
diff --git a/server/subsonic/api.go b/server/subsonic/api.go
index 9aceb612b..f49f5e731 100644
--- a/server/subsonic/api.go
+++ b/server/subsonic/api.go
@@ -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)
diff --git a/server/subsonic/library_scanning.go b/server/subsonic/library_scanning.go
new file mode 100644
index 000000000..ce78ef29e
--- /dev/null
+++ b/server/subsonic/library_scanning.go
@@ -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)
+}
diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses ScanStatus with data should match .JSON b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses ScanStatus with data should match .JSON
index 92b81193e..bbbbc5224 100644
--- a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses ScanStatus with data should match .JSON
+++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses ScanStatus with data should match .JSON
@@ -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"}}
diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses ScanStatus with data should match .XML b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses ScanStatus with data should match .XML
index e1029107f..826724e9e 100644
--- a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses ScanStatus with data should match .XML
+++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses ScanStatus with data should match .XML
@@ -1 +1 @@
-
+
diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Users with data should match .JSON b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Users with data should match .JSON
new file mode 100644
index 000000000..f2a9ec6be
--- /dev/null
+++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Users with data should match .JSON
@@ -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]}]}}
diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Users with data should match .XML b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Users with data should match .XML
new file mode 100644
index 000000000..1a3462072
--- /dev/null
+++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Users with data should match .XML
@@ -0,0 +1 @@
+1
diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Users without data should match .JSON b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Users without data should match .JSON
new file mode 100644
index 000000000..365eabd09
--- /dev/null
+++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Users without data should match .JSON
@@ -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}]}}
diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Users without data should match .XML b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Users without data should match .XML
new file mode 100644
index 000000000..a92c2e2fa
--- /dev/null
+++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Users without data should match .XML
@@ -0,0 +1 @@
+
diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go
index cc77f4e61..b2cf2b214 100644
--- a/server/subsonic/responses/responses.go
+++ b/server/subsonic/responses/responses.go
@@ -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"`
}
diff --git a/server/subsonic/responses/responses_test.go b/server/subsonic/responses/responses_test.go
index 1877fa67e..fc067fa37 100644
--- a/server/subsonic/responses/responses_test.go
+++ b/server/subsonic/responses/responses_test.go
@@ -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() {
diff --git a/server/subsonic/users.go b/server/subsonic/users.go
index e70370de1..a7c720df1 100644
--- a/server/subsonic/users.go
+++ b/server/subsonic/users.go
@@ -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
+}
diff --git a/server/subsonic/wire_gen.go b/server/subsonic/wire_gen.go
index 439e12108..dd7f3d24a 100644
--- a/server/subsonic/wire_gen.go
+++ b/server/subsonic/wire_gen.go
@@ -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"),
)
diff --git a/server/subsonic/wire_injectors.go b/server/subsonic/wire_injectors.go
index cc21188b2..99e76d843 100644
--- a/server/subsonic/wire_injectors.go
+++ b/server/subsonic/wire_injectors.go
@@ -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))
+}