mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
* refactor(req): drop redundant error return from Strings
The error from Strings carried no information beyond emptiness — it fired
exactly when the param was absent — and nearly every caller discarded it with
a blank identifier. Strings now just returns the values (empty when absent),
making the common optional-list reads one clean expression.
The few required-param callers (scrobble, createShare) check for emptiness and
return the same Subsonic error code 10 as before; their e2e tests now pin that
code. Ints and Times keep their contracts by synthesizing ErrMissingParam
themselves, so selectedMusicFolderIds is untouched. The jellyfin parseFields
helper is inlined away, since ParseFields(p.Strings("fields")...) now
compiles directly.
* docs(req): clarify Strings returns nil when param is absent
133 lines
3.9 KiB
Go
133 lines
3.9 KiB
Go
package subsonic
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
"github.com/navidrome/navidrome/server/subsonic/responses"
|
|
"github.com/navidrome/navidrome/utils/req"
|
|
)
|
|
|
|
func (api *Router) GetScanStatus(r *http.Request) (*responses.Subsonic, error) {
|
|
ctx := r.Context()
|
|
status, err := api.scanner.Status(ctx)
|
|
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: int64(status.Count),
|
|
FolderCount: int64(status.FolderCount),
|
|
LastScan: &status.LastScan,
|
|
Error: status.LastError,
|
|
ScanType: status.ScanType,
|
|
ElapsedTime: int64(status.ElapsedTime),
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func (api *Router) StartScan(r *http.Request) (*responses.Subsonic, error) {
|
|
ctx := r.Context()
|
|
loggedUser, ok := request.UserFrom(ctx)
|
|
if !ok {
|
|
return nil, newError(responses.ErrorGeneric, "Internal error")
|
|
}
|
|
|
|
p := req.Params(r)
|
|
fullScan := p.BoolOr("fullScan", false)
|
|
|
|
// Parse optional target parameters for selective scanning
|
|
var targets []model.ScanTarget
|
|
if targetParams := p.Strings("target"); len(targetParams) > 0 {
|
|
var err error
|
|
targets, err = model.ParseTargets(targetParams)
|
|
if err != nil {
|
|
return nil, newError(responses.ErrorGeneric, fmt.Sprintf("Invalid target parameter: %v", err))
|
|
}
|
|
|
|
// Validate all libraries in targets exist and user has access to them
|
|
userLibraries, err := api.ds.User(ctx).GetUserLibraries(loggedUser.ID)
|
|
if err != nil {
|
|
return nil, newError(responses.ErrorGeneric, "Internal error")
|
|
}
|
|
|
|
// Check each target library
|
|
for _, target := range targets {
|
|
if !slices.ContainsFunc(userLibraries, func(lib model.Library) bool { return lib.ID == target.LibraryID }) {
|
|
return nil, newError(responses.ErrorDataNotFound, fmt.Sprintf("Library with ID %d not found", target.LibraryID))
|
|
}
|
|
}
|
|
|
|
// Special case: if single library with empty path and it's the only library in DB, call ScanAll
|
|
if len(targets) == 1 && targets[0].FolderPath == "" {
|
|
allLibs, err := api.ds.Library(ctx).GetAll()
|
|
if err != nil {
|
|
return nil, newError(responses.ErrorGeneric, "Internal error")
|
|
}
|
|
if len(allLibs) == 1 {
|
|
targets = nil // This will trigger ScanAll below
|
|
}
|
|
}
|
|
}
|
|
|
|
fastScanCompleted := make(chan struct{})
|
|
go func() {
|
|
defer close(fastScanCompleted)
|
|
start := time.Now()
|
|
var err error
|
|
|
|
if len(targets) > 0 {
|
|
log.Info(ctx, "Triggering on-demand scan", "fullScan", fullScan, "targets", len(targets), "user", loggedUser.UserName)
|
|
_, err = api.scanner.ScanFolders(ctx, fullScan, targets)
|
|
} else {
|
|
log.Info(ctx, "Triggering on-demand scan", "fullScan", fullScan, "user", loggedUser.UserName)
|
|
_, err = api.scanner.ScanAll(ctx, fullScan)
|
|
}
|
|
|
|
if err != nil {
|
|
log.Error(ctx, "Error scanning", err)
|
|
return
|
|
}
|
|
log.Info(ctx, "On-demand scan complete", "user", loggedUser.UserName, "elapsed", time.Since(start))
|
|
}()
|
|
|
|
// Wait briefly for the scanner to start and update its status, so the response
|
|
// reflects the current scan (not stale data from a previous scan).
|
|
const (
|
|
pollInterval = 50 * time.Millisecond
|
|
pollTimeout = 3 * time.Second
|
|
)
|
|
ticker := time.NewTicker(pollInterval)
|
|
defer ticker.Stop()
|
|
timer := time.NewTimer(pollTimeout)
|
|
defer timer.Stop()
|
|
|
|
loop:
|
|
for {
|
|
status, err := api.scanner.Status(ctx)
|
|
if err == nil && status.Scanning {
|
|
break
|
|
}
|
|
select {
|
|
case <-fastScanCompleted:
|
|
log.Info(ctx, "Fast scan completed", "user", loggedUser.UserName)
|
|
break loop
|
|
case <-timer.C:
|
|
log.Warn(ctx, "Timed out waiting for scanner to start; response may be stale")
|
|
break loop
|
|
case <-ctx.Done():
|
|
return nil, newError(responses.ErrorGeneric, "Request cancelled while waiting for scanner to start")
|
|
case <-ticker.C:
|
|
}
|
|
}
|
|
|
|
return api.GetScanStatus(r)
|
|
}
|