feat: add DB-backed scanner entrypoint and compute-only main hook
Implements ScanArtist/ScanAll in internal/scanner loading local/external releases via the database layer with ctx-cancellation checks, plus a compute-only run() hook that logs missing-release counts. Add table-driven tests using in-memory SQLite fixtures.
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"naviwatcher/internal/config"
|
||||
"naviwatcher/internal/database"
|
||||
"naviwatcher/internal/musicbrainz"
|
||||
"naviwatcher/internal/scanner"
|
||||
)
|
||||
|
||||
// App holds all application dependencies for clean shutdown and testability.
|
||||
@@ -91,9 +92,27 @@ func (a *App) Close() {
|
||||
}
|
||||
|
||||
func (a *App) run(ctx context.Context) error {
|
||||
// Compute-only scanner hook: scan all monitored artists for missing
|
||||
// releases and log the count. Notifier/Web UI are out of scope for this
|
||||
// plan, so results are only logged. This call is non-blocking and
|
||||
// goroutine-safe; it observes ctx cancellation and returns early.
|
||||
missing, err := scanner.ScanAll(ctx, a.db, a.cfg.Scanner.FuzzyThreshold)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
// Context cancelled (e.g. shutdown) — exit cleanly.
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("scan all: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Scan complete: %d missing release(s) across monitored artists", len(missing))
|
||||
for _, m := range missing {
|
||||
log.Printf(" missing: artist=%s rgid=%s title=%q", m.ArtistID, m.RGID, m.Title)
|
||||
}
|
||||
|
||||
// Main application loop — blocks until context is cancelled.
|
||||
// Business logic (scanner, notifier, web server) will be wired into
|
||||
// separate goroutines here in future tasks.
|
||||
// Business logic (notifier, web server) will be wired into separate
|
||||
// goroutines here in future tasks.
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,8 +7,54 @@ import (
|
||||
"testing"
|
||||
|
||||
"naviwatcher/internal/config"
|
||||
"naviwatcher/internal/database"
|
||||
)
|
||||
|
||||
func TestAppRun_ScanLogsMissingReleases(t *testing.T) {
|
||||
// Verify the compute-only run() hook scans monitored artists and returns
|
||||
// nil without starting notifier/web. Uses an in-memory DB with one
|
||||
// monitored artist that has one missing release.
|
||||
db, err := database.New(":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("database.New() error: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if err := database.SaveArtistSettings(db, &database.ArtistSettings{
|
||||
ID: "artist-1",
|
||||
Name: "Pink Floyd",
|
||||
Monitored: true,
|
||||
}); err != nil {
|
||||
t.Fatalf("seed artist: %v", err)
|
||||
}
|
||||
if err := database.SaveLocalAlbum(db, &database.LocalAlbum{
|
||||
ID: "l1",
|
||||
ArtistID: "artist-1",
|
||||
Title: "The Wall",
|
||||
}); err != nil {
|
||||
t.Fatalf("seed local album: %v", err)
|
||||
}
|
||||
if err := database.SaveExternalRelease(db, &database.ExternalRelease{
|
||||
RGID: "rg2",
|
||||
ArtistID: "artist-1",
|
||||
Title: "Animals",
|
||||
}); err != nil {
|
||||
t.Fatalf("seed external release: %v", err)
|
||||
}
|
||||
|
||||
app := &App{
|
||||
cfg: &config.Config{Scanner: config.ScannerConfig{FuzzyThreshold: 0.85}},
|
||||
db: db,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // cancel immediately so run() exits after scanning
|
||||
|
||||
if err := app.run(ctx); err != nil {
|
||||
t.Fatalf("app.run() returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigIntegration(t *testing.T) {
|
||||
// Integration test: write a minimal valid config and load it via config.LoadConfig,
|
||||
// verifying the full path that main() uses.
|
||||
|
||||
Reference in New Issue
Block a user