feat: implement scanner caching and add HTMX loading state to library page
This commit is contained in:
parent
8dcde67884
commit
0ef3783e04
@ -6,16 +6,22 @@ import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"coda/internal/config"
|
||||
)
|
||||
|
||||
type Scanner struct {
|
||||
musicPath string
|
||||
mu sync.RWMutex
|
||||
cache []Album
|
||||
hasCached bool
|
||||
}
|
||||
|
||||
func New(cfg *config.Config) *Scanner {
|
||||
return &Scanner{musicPath: filepath.Clean(cfg.MusicPath)}
|
||||
return &Scanner{
|
||||
musicPath: filepath.Clean(cfg.MusicPath),
|
||||
}
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
@ -142,9 +148,20 @@ func (s *Scanner) Scan() ([]Album, error) {
|
||||
return albums[i].RelPath < albums[j].RelPath
|
||||
})
|
||||
|
||||
s.mu.Lock()
|
||||
s.cache = albums
|
||||
s.hasCached = true
|
||||
s.mu.Unlock()
|
||||
|
||||
return albums, nil
|
||||
}
|
||||
|
||||
func (s *Scanner) GetCached() ([]Album, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.cache, s.hasCached
|
||||
}
|
||||
|
||||
func (s *Scanner) Resolve(relPath string) (string, error) {
|
||||
resolved := filepath.Join(s.musicPath, relPath)
|
||||
resolved = filepath.Clean(resolved)
|
||||
|
||||
@ -202,3 +202,26 @@ func TestBrowseNonExistent(t *testing.T) {
|
||||
t.Error("expected error for nonexistent path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanCache(t *testing.T) {
|
||||
_, s := setupMusicDir(t)
|
||||
|
||||
albums, hasCache := s.GetCached()
|
||||
if hasCache || len(albums) != 0 {
|
||||
t.Fatal("expected no cache initially")
|
||||
}
|
||||
|
||||
scanned, err := s.Scan()
|
||||
if err != nil {
|
||||
t.Fatalf("Scan: %v", err)
|
||||
}
|
||||
|
||||
albums, hasCache = s.GetCached()
|
||||
if !hasCache {
|
||||
t.Fatal("expected cache to be populated after Scan")
|
||||
}
|
||||
|
||||
if len(albums) != len(scanned) {
|
||||
t.Errorf("cached count = %d, want %d", len(albums), len(scanned))
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,16 +10,28 @@ import (
|
||||
|
||||
"coda/internal/jobs"
|
||||
"coda/internal/models"
|
||||
"coda/internal/scanner"
|
||||
)
|
||||
|
||||
type LibraryPageData struct {
|
||||
Entries []scanner.Entry
|
||||
CachedAlbums []scanner.Album
|
||||
HasCache bool
|
||||
}
|
||||
|
||||
func (h *Handler) Library(w http.ResponseWriter, r *http.Request) {
|
||||
entries, err := h.scanner.Browse("")
|
||||
if err != nil {
|
||||
slog.Warn("library page browse failed, showing empty view", "error", err)
|
||||
h.render.Render(w, "library.html", nil)
|
||||
return
|
||||
}
|
||||
h.render.Render(w, "library.html", entries)
|
||||
|
||||
cachedAlbums, hasCache := h.scanner.GetCached()
|
||||
data := LibraryPageData{
|
||||
Entries: entries,
|
||||
CachedAlbums: cachedAlbums,
|
||||
HasCache: hasCache,
|
||||
}
|
||||
h.render.Render(w, "library.html", data)
|
||||
}
|
||||
|
||||
func (h *Handler) LibraryBrowse(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@ -147,6 +147,60 @@ func TestLibraryPage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibraryPageCacheRendering(t *testing.T) {
|
||||
musicPath := t.TempDir()
|
||||
albumDir := filepath.Join(musicPath, "Test Artist", "Test Album")
|
||||
os.MkdirAll(albumDir, 0o755)
|
||||
|
||||
flacFile := filepath.Join(albumDir, "track.flac")
|
||||
if err := os.WriteFile(flacFile, []byte("fake"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "test.db")
|
||||
st, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("store.New: %v", err)
|
||||
}
|
||||
defer st.Close()
|
||||
|
||||
svc, err := config.NewService(context.Background(), &mockStore{data: map[string]string{
|
||||
"MUSIC_PATH": musicPath,
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("config.NewService: %v", err)
|
||||
}
|
||||
|
||||
q := queue.New(100)
|
||||
jobSvc := jobs.New(st, q)
|
||||
sc := scanner.New(&config.Config{MusicPath: musicPath})
|
||||
|
||||
if _, err := sc.Scan(); err != nil {
|
||||
t.Fatalf("Scan: %v", err)
|
||||
}
|
||||
|
||||
router := Router(Deps{
|
||||
Cfg: svc,
|
||||
Store: st,
|
||||
Queue: q,
|
||||
Scanner: sc,
|
||||
Enqueuer: jobSvc,
|
||||
})
|
||||
|
||||
rec := get(t, router, "/library")
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, "Test Album") {
|
||||
t.Error("expected cached album 'Test Album' to be rendered on library page load")
|
||||
}
|
||||
if !strings.Contains(body, "Test Artist") {
|
||||
t.Error("expected cached artist 'Test Artist' to be rendered on library page load")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsPage(t *testing.T) {
|
||||
router := setupRouter(t)
|
||||
rec := get(t, router, "/settings")
|
||||
|
||||
0
testdata/music/ArtistA/Album1/song.flac
vendored
Normal file
0
testdata/music/ArtistA/Album1/song.flac
vendored
Normal file
@ -4,3 +4,15 @@
|
||||
@source not "../../tools";
|
||||
|
||||
@plugin "../../tools/daisyui.mjs";
|
||||
|
||||
.htmx-indicator {
|
||||
display: none;
|
||||
}
|
||||
.htmx-request.htmx-indicator,
|
||||
.htmx-request .htmx-indicator {
|
||||
display: inline-block;
|
||||
}
|
||||
.htmx-request#scan-results {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@ -2,7 +2,12 @@
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold">Library</h1>
|
||||
<button class="btn btn-primary" hx-get="/api/library/scan" hx-target="#scan-results">
|
||||
<button class="btn btn-primary animate-none"
|
||||
hx-get="/api/library/scan"
|
||||
hx-target="#scan-results"
|
||||
hx-indicator="#scan-spinner, #scan-results"
|
||||
hx-disabled-elt="this">
|
||||
<span id="scan-spinner" class="loading loading-spinner loading-xs htmx-indicator mr-1"></span>
|
||||
Scan for FLACs
|
||||
</button>
|
||||
</div>
|
||||
@ -11,7 +16,11 @@
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Scan Results</h2>
|
||||
<div id="scan-results" class="overflow-x-auto">
|
||||
<p class="text-sm opacity-50">Click scan to find FLAC albums</p>
|
||||
{{ if .HasCache }}
|
||||
{{ template "scan_results.html" .CachedAlbums }}
|
||||
{{ else }}
|
||||
<p class="text-sm opacity-50">Click scan to find FLAC albums</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user