feat: the library's folders are set in Admin › Library, and changing a folder moves its tracks with it, keeping their plays, likes and playlists (#6129)
release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m20s
release / go (push) Failing after 1m37s
release / integration (push) Failing after 4m56s
release / android (push) Successful in 5m1s
release / Build signed APK (releases and dev) (push) Successful in 5m13s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m20s
release / go (push) Failing after 1m37s
release / integration (push) Failing after 4m56s
release / android (push) Successful in 5m1s
release / Build signed APK (releases and dev) (push) Successful in 5m13s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user