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.
119 lines
3.1 KiB
Go
119 lines
3.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
"naviwatcher/internal/config"
|
|
"naviwatcher/internal/database"
|
|
"naviwatcher/internal/musicbrainz"
|
|
"naviwatcher/internal/scanner"
|
|
)
|
|
|
|
// App holds all application dependencies for clean shutdown and testability.
|
|
type App struct {
|
|
cfg *config.Config
|
|
db *database.DB
|
|
mbClient *musicbrainz.MusicBrainzClient
|
|
}
|
|
|
|
const defaultConfigPath = "config.yaml"
|
|
|
|
func main() {
|
|
configPath := flag.String("config", defaultConfigPath, "Path to config file")
|
|
flag.Parse()
|
|
|
|
log.Println("NaviWatcher starting...")
|
|
|
|
cfg, err := config.LoadConfig(*configPath)
|
|
if err != nil {
|
|
log.Fatalf("Failed to load config: %v", err)
|
|
}
|
|
|
|
log.Printf("Config loaded from %s (server: %s:%d)", *configPath, cfg.Server.Host, cfg.Server.Port)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
sigCh := make(chan os.Signal, 1)
|
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
go func() {
|
|
sig := <-sigCh
|
|
log.Printf("Received signal %v, shutting down...", sig)
|
|
cancel()
|
|
}()
|
|
|
|
app, err := NewApp(ctx, cfg)
|
|
if err != nil {
|
|
log.Fatalf("Failed to initialize application: %v", err)
|
|
}
|
|
defer app.Close()
|
|
|
|
if err := app.run(ctx); err != nil {
|
|
log.Fatalf("Application error: %v", err)
|
|
}
|
|
|
|
log.Println("NaviWatcher stopped.")
|
|
}
|
|
|
|
// NewApp initializes all application components: config, database, and MusicBrainz client.
|
|
func NewApp(ctx context.Context, cfg *config.Config) (*App, error) {
|
|
// Initialize database (uses default path or could be made configurable).
|
|
db, err := database.New("naviwatcher.db")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to initialize database: %w", err)
|
|
}
|
|
|
|
// Initialize MusicBrainz client with rate limiting.
|
|
mbClient := musicbrainz.NewClient(cfg.MusicBrainz)
|
|
|
|
log.Printf("MusicBrainz client initialized (user-agent: %s)", cfg.MusicBrainz.UserAgent)
|
|
|
|
return &App{
|
|
cfg: cfg,
|
|
db: db,
|
|
mbClient: mbClient,
|
|
}, nil
|
|
}
|
|
|
|
// Close cleans up all application resources in reverse order of initialization.
|
|
func (a *App) Close() {
|
|
if a.db != nil {
|
|
if err := a.db.Close(); err != nil {
|
|
log.Printf("Error closing database: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 (notifier, web server) will be wired into separate
|
|
// goroutines here in future tasks.
|
|
<-ctx.Done()
|
|
return nil
|
|
}
|