package cmd import ( "context" "database/sql" "fmt" "io" "os" "github.com/navidrome/navidrome/db" "github.com/spf13/cobra" ) func init() { rootCmd.AddCommand(doctorCmd) } var doctorCmd = &cobra.Command{ Use: "doctor", Short: "Check your Navidrome installation for problems", Long: "Run read-only health checks and report what was found. Checks the database for " + "corruption and foreign key violations, and reports whether 'navidrome search rebuild' " + "can fix what it finds. This command never alters your data", Run: func(cmd *cobra.Command, _ []string) { runDoctor(cmd.Context()) }, } func runDoctor(ctx context.Context) { requireExistingDB() healthy := doctor(ctx, db.Db(), os.Stdout) db.Close(ctx) if !healthy { os.Exit(1) } } const recoveryAdvice = "Restore a backup (navidrome backup restore), or try SQLite's '.recover' command." func printFindings(out io.Writer, check, noun string, items []string) { fmt.Fprintf(out, "%s reported %d %s:\n", check, len(items), noun) for _, item := range items { fmt.Fprintln(out, " "+item) } } func doctor(ctx context.Context, database *sql.DB, out io.Writer) bool { healthy := true fmt.Fprintln(out, "Checking database integrity...") issues, truncated, err := db.IntegrityCheck(ctx, database) switch { case err != nil: fmt.Fprintln(out, "The integrity check could not complete: "+err.Error()) fmt.Fprintln(out, recoveryAdvice) return false case len(issues) == 0: fmt.Fprintln(out, "Integrity check passed.") default: healthy = false printFindings(out, "Integrity check", "issue(s)", issues) switch { case truncated: fmt.Fprintln(out, "The integrity check stopped at its limit, so the damage may reach further than listed.") fmt.Fprintln(out, recoveryAdvice) case db.IsFTSCorruptionOnly(issues): fmt.Fprintln(out, "Corruption is limited to the search index. Run 'navidrome search rebuild' to fix it.") default: fmt.Fprintln(out, "Corruption is not limited to the search index, and cannot be repaired automatically.") fmt.Fprintln(out, recoveryAdvice) } } fmt.Fprintln(out, "Checking foreign keys...") violations, err := db.ForeignKeyCheck(ctx, database) switch { case err != nil: healthy = false fmt.Fprintln(out, "The foreign key check could not complete: "+err.Error()) case len(violations) == 0: fmt.Fprintln(out, "Foreign key check passed.") default: healthy = false lines := make([]string, 0, len(violations)) for _, v := range violations { lines = append(lines, fmt.Sprintf("%s: %d row(s) reference missing rows in %s", v.Table, v.Count, v.Parent)) } printFindings(out, "Foreign key check", "violation(s)", lines) fmt.Fprintln(out, "These are orphaned rows, not corruption. 'navidrome scan -f' clears some of them "+ "in library data; the rest have to be removed by hand.") } if healthy { fmt.Fprintln(out, "Database is healthy.") } return healthy }