navidrome/server/nativeapi/missing.go
Deluan 593b5db8e9 refactor: extract missing files deletion into reusable service layer
Extracted inline deletion logic from server/nativeapi/missing.go into a new core.MissingFiles service interface and implementation. This provides better separation of concerns and testability.

The MissingFiles service handles:
- Deletion of specific or all missing files via transaction
- Garbage collection after deletion
- Extraction of affected album IDs from missing files
- Background refresh of artist and album statistics

The deleteMissingFiles HTTP handler now simply delegates to the service, removing 70+ lines of inline logic. All deletion, transaction, and stat refresh logic is now centralized in core/missing_files.go.

Updated dependency injection to provide MissingFiles service to the native API router. Renamed receiver variable from 'n' to 'api' throughout native_api.go for consistency.
2025-11-08 15:50:43 -05:00

94 lines
2.3 KiB
Go

package nativeapi
import (
"context"
"errors"
"maps"
"net/http"
"github.com/Masterminds/squirrel"
"github.com/deluan/rest"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/req"
)
type missingRepository struct {
model.ResourceRepository
mfRepo model.MediaFileRepository
}
func newMissingRepository(ds model.DataStore) rest.RepositoryConstructor {
return func(ctx context.Context) rest.Repository {
return &missingRepository{mfRepo: ds.MediaFile(ctx), ResourceRepository: ds.Resource(ctx, model.MediaFile{})}
}
}
func (r *missingRepository) Count(options ...rest.QueryOptions) (int64, error) {
opt := r.parseOptions(options)
return r.ResourceRepository.Count(opt)
}
func (r *missingRepository) ReadAll(options ...rest.QueryOptions) (any, error) {
opt := r.parseOptions(options)
return r.ResourceRepository.ReadAll(opt)
}
func (r *missingRepository) parseOptions(options []rest.QueryOptions) rest.QueryOptions {
var opt rest.QueryOptions
if len(options) > 0 {
opt = options[0]
opt.Filters = maps.Clone(opt.Filters)
}
opt.Filters["missing"] = "true"
return opt
}
func (r *missingRepository) Read(id string) (any, error) {
all, err := r.mfRepo.GetAll(model.QueryOptions{Filters: squirrel.And{
squirrel.Eq{"id": id},
squirrel.Eq{"missing": true},
}})
if err != nil {
return nil, err
}
if len(all) == 0 {
return nil, model.ErrNotFound
}
return all[0], nil
}
func (r *missingRepository) EntityName() string {
return "missing_files"
}
func deleteMissingFiles(missingFiles core.MissingFiles) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
p := req.Params(r)
ids, _ := p.Strings("id")
var err error
if len(ids) == 0 {
err = missingFiles.DeleteAllMissingFiles(ctx)
} else {
err = missingFiles.DeleteMissingFiles(ctx, ids)
}
if len(ids) == 1 && errors.Is(err, model.ErrNotFound) {
log.Warn(ctx, "Missing file not found", "id", ids[0])
http.Error(w, "not found", http.StatusNotFound)
return
}
if err != nil {
http.Error(w, "failed to delete missing files", http.StatusInternalServerError)
return
}
writeDeleteManyResponse(w, r, ids)
}
}
var _ model.ResourceRepository = &missingRepository{}