Files
NaviWatcher/cmd/naviwatcher/main.go

122 lines
3.2 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.mbClient != nil {
a.mbClient.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. ScanAll is a blocking DB walk over
// every monitored artist; 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
}