package api import ( "context" "errors" "net/http" "os" "git.fabledsword.com/bvandeusen/minstrel/internal/apierror" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" "git.fabledsword.com/bvandeusen/minstrel/internal/library" ) // The library's folders (#6129): where the scan, the watcher and the import // look for music, set here rather than only by MINSTREL_LIBRARY_SCAN_PATHS. type libraryFolderView struct { Path string `json:"path"` // Present is whether the server can see the folder right now; a folder // whose mount went away reads false here before anything else notices. Present bool `json:"present"` Tracks int64 `json:"tracks"` } type libraryFoldersView struct { Folders []libraryFolderView `json:"folders"` // Saved is false while the folders still come from // MINSTREL_LIBRARY_SCAN_PATHS. Saved bool `json:"saved"` // Moved is how many tracks a save moved to a new folder. Moved int64 `json:"moved,omitempty"` } func (h *handlers) libraryFolders(ctx context.Context) (libraryFoldersView, error) { q := dbq.New(h.pool) row, err := q.GetLibrarySettings(ctx) if err != nil { return libraryFoldersView{}, err } v := libraryFoldersView{Folders: []libraryFolderView{}, Saved: len(row.ScanPaths) > 0} for _, root := range h.scanner.Roots() { n, err := q.CountTracksUnder(ctx, root) if err != nil { return libraryFoldersView{}, err } info, statErr := os.Stat(root) v.Folders = append(v.Folders, libraryFolderView{Path: root, Present: statErr == nil && info.IsDir(), Tracks: n}) } return v, nil } // handleGetLibraryFolders implements GET /api/admin/library/folders. func (h *handlers) handleGetLibraryFolders(w http.ResponseWriter, r *http.Request) { v, err := h.libraryFolders(r.Context()) if err != nil { writeErrWithLog(w, h.logger, "admin library folders: get", apierror.Internal(err)) return } writeJSON(w, http.StatusOK, v) } type putLibraryFoldersBody struct { Folders []struct { Path string `json:"path"` // Was is the folder this one replaces, whose files are now at Path; // "" is a folder added. Was string `json:"was"` } `json:"folders"` } // handlePutLibraryFolders implements PUT /api/admin/library/folders. // // The whole list. A folder whose path changed moves its tracks with it, so // their plays, likes and playlists survive; one left out is no longer read, // and its tracks are marked missing by the next scan. After a save a scan // starts, to pick up anything the old folders did not hold. func (h *handlers) handlePutLibraryFolders(w http.ResponseWriter, r *http.Request) { var body putLibraryFoldersBody if !decodeBody(w, r, &body) { return } if h.scanner == nil { writeErr(w, apierror.Internal(errors.New("no scanner"))) return } changes := make([]library.RootChange, 0, len(body.Folders)) for _, f := range body.Folders { changes = append(changes, library.RootChange{Path: f.Path, Was: f.Was}) } res, err := h.scanner.SetRoots(r.Context(), changes) if err != nil { var re *library.RootsError if errors.As(err, &re) { writeErr(w, apierror.BadRequest("bad_folders", re.Error())) return } writeErrWithLog(w, h.logger, "admin library folders: put", apierror.Internal(err)) return } if _, _, err := library.TryStartScan(context.Background(), h.pool, h.scanner, h.coverart, h.logger.With("source", "library_folders"), h.scanCfg); err != nil { h.logger.Warn("admin library folders: scan after save", "err", err) } v, err := h.libraryFolders(r.Context()) if err != nil { writeErrWithLog(w, h.logger, "admin library folders: get", apierror.Internal(err)) return } v.Moved = res.Tracks writeJSON(w, http.StatusOK, v) }