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)) +}