diff --git a/cmd/backup.go b/cmd/backup.go index ab73f7537..aa962e258 100644 --- a/cmd/backup.go +++ b/cmd/backup.go @@ -1,186 +1,187 @@ package cmd -import ( - "context" - "fmt" - "os" - "strings" - "time" - - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/db" - "github.com/navidrome/navidrome/log" - "github.com/spf13/cobra" -) - -var ( - backupCount int - backupDir string - force bool - restorePath string -) - -func init() { - rootCmd.AddCommand(backupRoot) - - backupCmd.Flags().StringVarP(&backupDir, "backup-dir", "d", "", "directory to manually make backup") - backupRoot.AddCommand(backupCmd) - - pruneCmd.Flags().StringVarP(&backupDir, "backup-dir", "d", "", "directory holding Navidrome backups") - pruneCmd.Flags().IntVarP(&backupCount, "keep-count", "k", -1, "specify the number of backups to keep. 0 remove ALL backups, and negative values mean to use the default from configuration") - pruneCmd.Flags().BoolVarP(&force, "force", "f", false, "bypass warning when backup count is zero") - backupRoot.AddCommand(pruneCmd) - - restoreCommand.Flags().StringVarP(&restorePath, "backup-file", "b", "", "path of backup database to restore") - restoreCommand.Flags().BoolVarP(&force, "force", "f", false, "bypass restore warning") - _ = restoreCommand.MarkFlagRequired("backup-file") - backupRoot.AddCommand(restoreCommand) -} - -var ( - backupRoot = &cobra.Command{ - Use: "backup", - Aliases: []string{"bkp"}, - Short: "Create, restore and prune database backups", - Long: "Create, restore and prune database backups", - } - - backupCmd = &cobra.Command{ - Use: "create", - Short: "Create a backup database", - Long: "Manually backup Navidrome database. This will ignore BackupCount", - Run: func(cmd *cobra.Command, _ []string) { - runBackup(cmd.Context()) - }, - } - - pruneCmd = &cobra.Command{ - Use: "prune", - Short: "Prune database backups", - Long: "Manually prune database backups according to backup rules", - Run: func(cmd *cobra.Command, _ []string) { - runPrune(cmd.Context()) - }, - } - - restoreCommand = &cobra.Command{ - Use: "restore", - Short: "Restore Navidrome database", - Long: "Restore Navidrome database from a backup. This must be done offline", - Run: func(cmd *cobra.Command, _ []string) { - runRestore(cmd.Context()) - }, - } -) - -func runBackup(ctx context.Context) { - if backupDir != "" { - conf.Server.Backup.Path = backupDir - } - - idx := strings.LastIndex(conf.Server.DbPath, "?") - var path string - - if idx == -1 { - path = conf.Server.DbPath - } else { - path = conf.Server.DbPath[:idx] - } - - if _, err := os.Stat(path); os.IsNotExist(err) { - log.Fatal("No existing database", "path", path) - return - } - - start := time.Now() - path, err := db.Backup(ctx) - if err != nil { - log.Fatal("Error backing up database", "backup path", conf.Server.BasePath, err) - } - - elapsed := time.Since(start) - log.Info("Backup complete", "elapsed", elapsed, "path", path) -} - -func runPrune(ctx context.Context) { - if backupDir != "" { - conf.Server.Backup.Path = backupDir - } - - if backupCount != -1 { - conf.Server.Backup.Count = backupCount - } - - if conf.Server.Backup.Count == 0 && !force { - fmt.Println("Warning: pruning ALL backups") - fmt.Printf("Please enter YES (all caps) to continue: ") - var input string - _, err := fmt.Scanln(&input) - - if input != "YES" || err != nil { - log.Warn("Prune cancelled") - return - } - } - - idx := strings.LastIndex(conf.Server.DbPath, "?") - var path string - - if idx == -1 { - path = conf.Server.DbPath - } else { - path = conf.Server.DbPath[:idx] - } - - if _, err := os.Stat(path); os.IsNotExist(err) { - log.Fatal("No existing database", "path", path) - return - } - - start := time.Now() - count, err := db.Prune(ctx) - if err != nil { - log.Fatal("Error pruning up database", "backup path", conf.Server.BasePath, err) - } - - elapsed := time.Since(start) - - log.Info("Prune complete", "elapsed", elapsed, "successfully pruned", count) -} - -func runRestore(ctx context.Context) { - idx := strings.LastIndex(conf.Server.DbPath, "?") - var path string - - if idx == -1 { - path = conf.Server.DbPath - } else { - path = conf.Server.DbPath[:idx] - } - - if _, err := os.Stat(path); os.IsNotExist(err) { - log.Fatal("No existing database", "path", path) - return - } - - if !force { - fmt.Println("Warning: restoring the Navidrome database should only be done offline, especially if your backup is very old.") - fmt.Printf("Please enter YES (all caps) to continue: ") - var input string - _, err := fmt.Scanln(&input) - - if input != "YES" || err != nil { - log.Warn("Restore cancelled") - return - } - } - - start := time.Now() - err := db.Restore(ctx, restorePath) - if err != nil { - log.Fatal("Error restoring database", "backup path", conf.Server.BasePath, err) - } - - elapsed := time.Since(start) - log.Info("Restore complete", "elapsed", elapsed) -} +// +//import ( +// "context" +// "fmt" +// "os" +// "strings" +// "time" +// +// "github.com/navidrome/navidrome/conf" +// "github.com/navidrome/navidrome/db" +// "github.com/navidrome/navidrome/log" +// "github.com/spf13/cobra" +//) +// +//var ( +// backupCount int +// backupDir string +// force bool +// restorePath string +//) +// +//func init() { +// rootCmd.AddCommand(backupRoot) +// +// backupCmd.Flags().StringVarP(&backupDir, "backup-dir", "d", "", "directory to manually make backup") +// backupRoot.AddCommand(backupCmd) +// +// pruneCmd.Flags().StringVarP(&backupDir, "backup-dir", "d", "", "directory holding Navidrome backups") +// pruneCmd.Flags().IntVarP(&backupCount, "keep-count", "k", -1, "specify the number of backups to keep. 0 remove ALL backups, and negative values mean to use the default from configuration") +// pruneCmd.Flags().BoolVarP(&force, "force", "f", false, "bypass warning when backup count is zero") +// backupRoot.AddCommand(pruneCmd) +// +// restoreCommand.Flags().StringVarP(&restorePath, "backup-file", "b", "", "path of backup database to restore") +// restoreCommand.Flags().BoolVarP(&force, "force", "f", false, "bypass restore warning") +// _ = restoreCommand.MarkFlagRequired("backup-file") +// backupRoot.AddCommand(restoreCommand) +//} +// +//var ( +// backupRoot = &cobra.Command{ +// Use: "backup", +// Aliases: []string{"bkp"}, +// Short: "Create, restore and prune database backups", +// Long: "Create, restore and prune database backups", +// } +// +// backupCmd = &cobra.Command{ +// Use: "create", +// Short: "Create a backup database", +// Long: "Manually backup Navidrome database. This will ignore BackupCount", +// Run: func(cmd *cobra.Command, _ []string) { +// runBackup(cmd.Context()) +// }, +// } +// +// pruneCmd = &cobra.Command{ +// Use: "prune", +// Short: "Prune database backups", +// Long: "Manually prune database backups according to backup rules", +// Run: func(cmd *cobra.Command, _ []string) { +// runPrune(cmd.Context()) +// }, +// } +// +// restoreCommand = &cobra.Command{ +// Use: "restore", +// Short: "Restore Navidrome database", +// Long: "Restore Navidrome database from a backup. This must be done offline", +// Run: func(cmd *cobra.Command, _ []string) { +// runRestore(cmd.Context()) +// }, +// } +//) +// +//func runBackup(ctx context.Context) { +// if backupDir != "" { +// conf.Server.Backup.Path = backupDir +// } +// +// idx := strings.LastIndex(conf.Server.DbPath, "?") +// var path string +// +// if idx == -1 { +// path = conf.Server.DbPath +// } else { +// path = conf.Server.DbPath[:idx] +// } +// +// if _, err := os.Stat(path); os.IsNotExist(err) { +// log.Fatal("No existing database", "path", path) +// return +// } +// +// start := time.Now() +// path, err := db.Backup(ctx) +// if err != nil { +// log.Fatal("Error backing up database", "backup path", conf.Server.BasePath, err) +// } +// +// elapsed := time.Since(start) +// log.Info("Backup complete", "elapsed", elapsed, "path", path) +//} +// +//func runPrune(ctx context.Context) { +// if backupDir != "" { +// conf.Server.Backup.Path = backupDir +// } +// +// if backupCount != -1 { +// conf.Server.Backup.Count = backupCount +// } +// +// if conf.Server.Backup.Count == 0 && !force { +// fmt.Println("Warning: pruning ALL backups") +// fmt.Printf("Please enter YES (all caps) to continue: ") +// var input string +// _, err := fmt.Scanln(&input) +// +// if input != "YES" || err != nil { +// log.Warn("Prune cancelled") +// return +// } +// } +// +// idx := strings.LastIndex(conf.Server.DbPath, "?") +// var path string +// +// if idx == -1 { +// path = conf.Server.DbPath +// } else { +// path = conf.Server.DbPath[:idx] +// } +// +// if _, err := os.Stat(path); os.IsNotExist(err) { +// log.Fatal("No existing database", "path", path) +// return +// } +// +// start := time.Now() +// count, err := db.Prune(ctx) +// if err != nil { +// log.Fatal("Error pruning up database", "backup path", conf.Server.BasePath, err) +// } +// +// elapsed := time.Since(start) +// +// log.Info("Prune complete", "elapsed", elapsed, "successfully pruned", count) +//} +// +//func runRestore(ctx context.Context) { +// idx := strings.LastIndex(conf.Server.DbPath, "?") +// var path string +// +// if idx == -1 { +// path = conf.Server.DbPath +// } else { +// path = conf.Server.DbPath[:idx] +// } +// +// if _, err := os.Stat(path); os.IsNotExist(err) { +// log.Fatal("No existing database", "path", path) +// return +// } +// +// if !force { +// fmt.Println("Warning: restoring the Navidrome database should only be done offline, especially if your backup is very old.") +// fmt.Printf("Please enter YES (all caps) to continue: ") +// var input string +// _, err := fmt.Scanln(&input) +// +// if input != "YES" || err != nil { +// log.Warn("Restore cancelled") +// return +// } +// } +// +// start := time.Now() +// err := db.Restore(ctx, restorePath) +// if err != nil { +// log.Fatal("Error restoring database", "backup path", conf.Server.BasePath, err) +// } +// +// elapsed := time.Since(start) +// log.Info("Restore complete", "elapsed", elapsed) +//} diff --git a/cmd/root.go b/cmd/root.go index 9618b16e6..515b24137 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -16,7 +16,6 @@ import ( "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/resources" - "github.com/navidrome/navidrome/scanner" "github.com/navidrome/navidrome/scheduler" "github.com/navidrome/navidrome/server/backgrounds" "github.com/spf13/cobra" @@ -81,7 +80,6 @@ func runNavidrome(ctx context.Context) { g.Go(startPlaybackServer(ctx)) g.Go(schedulePeriodicBackup(ctx)) g.Go(startInsightsCollector(ctx)) - g.Go(scheduleDBOptimizer(ctx)) g.Go(startPluginManager(ctx)) g.Go(runInitialScan(ctx)) if conf.Server.Scanner.Enabled { @@ -236,51 +234,37 @@ func startScanWatcher(ctx context.Context) func() error { func schedulePeriodicBackup(ctx context.Context) func() error { return func() error { - schedule := conf.Server.Backup.Schedule - if schedule == "" { - log.Info(ctx, "Periodic backup is DISABLED") - return nil - } - - schedulerInstance := scheduler.GetInstance() - - log.Info("Scheduling periodic backup", "schedule", schedule) - _, err := schedulerInstance.Add(schedule, func() { - start := time.Now() - path, err := db.Backup(ctx) - elapsed := time.Since(start) - if err != nil { - log.Error(ctx, "Error backing up database", "elapsed", elapsed, err) - return - } - log.Info(ctx, "Backup complete", "elapsed", elapsed, "path", path) - - count, err := db.Prune(ctx) - if err != nil { - log.Error(ctx, "Error pruning database", "error", err) - } else if count > 0 { - log.Info(ctx, "Successfully pruned old files", "count", count) - } else { - log.Info(ctx, "No backups pruned") - } - }) - - return err - } -} - -func scheduleDBOptimizer(ctx context.Context) func() error { - return func() error { - log.Info(ctx, "Scheduling DB optimizer", "schedule", consts.OptimizeDBSchedule) - schedulerInstance := scheduler.GetInstance() - _, err := schedulerInstance.Add(consts.OptimizeDBSchedule, func() { - if scanner.IsScanning() { - log.Debug(ctx, "Skipping DB optimization because a scan is in progress") - return - } - db.Optimize(ctx) - }) - return err + //schedule := conf.Server.Backup.Schedule + //if schedule == "" { + // log.Info(ctx, "Periodic backup is DISABLED") + // return nil + //} + // + //schedulerInstance := scheduler.GetInstance() + // + //log.Info("Scheduling periodic backup", "schedule", schedule) + //_, err := schedulerInstance.Add(schedule, func() { + // start := time.Now() + // path, err := db.Backup(ctx) + // elapsed := time.Since(start) + // if err != nil { + // log.Error(ctx, "Error backing up database", "elapsed", elapsed, err) + // return + // } + // log.Info(ctx, "Backup complete", "elapsed", elapsed, "path", path) + // + // count, err := db.Prune(ctx) + // if err != nil { + // log.Error(ctx, "Error pruning database", "error", err) + // } else if count > 0 { + // log.Info(ctx, "Successfully pruned old files", "count", count) + // } else { + // log.Info(ctx, "No backups pruned") + // } + //}) + // + //return err + return nil } } diff --git a/cmd/scan.go b/cmd/scan.go index d37ccd69f..681bdcdf0 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -63,6 +63,8 @@ func trackScanAsSubprocess(ctx context.Context, progress <-chan *scanner.Progres } func runScanner(ctx context.Context) { + defer db.Init(ctx)() + sqlDB := db.Db() defer db.Db().Close() ds := persistence.New(sqlDB) diff --git a/core/artwork/reader_artist.go b/core/artwork/reader_artist.go index cb029a16e..c8ce28aa4 100644 --- a/core/artwork/reader_artist.go +++ b/core/artwork/reader_artist.go @@ -44,7 +44,7 @@ func newArtistArtworkReader(ctx context.Context, artwork *artwork, artID model.A als, err := artwork.ds.Album(ctx).GetAll(model.QueryOptions{ Filters: squirrel.And{ squirrel.Eq{"album_artist_id": artID.ID}, - squirrel.Eq{"json_array_length(participants, '$.albumartist')": 1}, + squirrel.Eq{"jsonb_array_length(participants->'albumartist')": 1}, }, }) if err != nil { diff --git a/db/backup.go b/db/backup.go index 8b0f18b1b..c8f5fb396 100644 --- a/db/backup.go +++ b/db/backup.go @@ -1,167 +1,168 @@ package db -import ( - "context" - "database/sql" - "errors" - "fmt" - "os" - "path/filepath" - "regexp" - "slices" - "time" - - "github.com/mattn/go-sqlite3" - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/log" -) - -const ( - backupPrefix = "navidrome_backup" - backupRegexString = backupPrefix + "_(.+)\\.db" -) - -var backupRegex = regexp.MustCompile(backupRegexString) - -const backupSuffixLayout = "2006.01.02_15.04.05" - -func backupPath(t time.Time) string { - return filepath.Join( - conf.Server.Backup.Path, - fmt.Sprintf("%s_%s.db", backupPrefix, t.Format(backupSuffixLayout)), - ) -} - -func backupOrRestore(ctx context.Context, isBackup bool, path string) error { - // heavily inspired by https://codingrabbits.dev/posts/go_and_sqlite_backup_and_maybe_restore/ - existingConn, err := Db().Conn(ctx) - if err != nil { - return fmt.Errorf("getting existing connection: %w", err) - } - defer existingConn.Close() - - backupDb, err := sql.Open(Driver, path) - if err != nil { - return fmt.Errorf("opening backup database in '%s': %w", path, err) - } - defer backupDb.Close() - - backupConn, err := backupDb.Conn(ctx) - if err != nil { - return fmt.Errorf("getting backup connection: %w", err) - } - defer backupConn.Close() - - err = existingConn.Raw(func(existing any) error { - return backupConn.Raw(func(backup any) error { - var sourceOk, destOk bool - var sourceConn, destConn *sqlite3.SQLiteConn - - if isBackup { - sourceConn, sourceOk = existing.(*sqlite3.SQLiteConn) - destConn, destOk = backup.(*sqlite3.SQLiteConn) - } else { - sourceConn, sourceOk = backup.(*sqlite3.SQLiteConn) - destConn, destOk = existing.(*sqlite3.SQLiteConn) - } - - if !sourceOk { - return fmt.Errorf("error trying to convert source to sqlite connection") - } - if !destOk { - return fmt.Errorf("error trying to convert destination to sqlite connection") - } - - backupOp, err := destConn.Backup("main", sourceConn, "main") - if err != nil { - return fmt.Errorf("error starting sqlite backup: %w", err) - } - defer backupOp.Close() - - // Caution: -1 means that sqlite will hold a read lock until the operation finishes - // This will lock out other writes that could happen at the same time - done, err := backupOp.Step(-1) - if !done { - return fmt.Errorf("backup not done with step -1") - } - if err != nil { - return fmt.Errorf("error during backup step: %w", err) - } - - err = backupOp.Finish() - if err != nil { - return fmt.Errorf("error finishing backup: %w", err) - } - - return nil - }) - }) - - return err -} - -func Backup(ctx context.Context) (string, error) { - destPath := backupPath(time.Now()) - log.Debug(ctx, "Creating backup", "path", destPath) - err := backupOrRestore(ctx, true, destPath) - if err != nil { - return "", err - } - - return destPath, nil -} - -func Restore(ctx context.Context, path string) error { - log.Debug(ctx, "Restoring backup", "path", path) - return backupOrRestore(ctx, false, path) -} - -func Prune(ctx context.Context) (int, error) { - files, err := os.ReadDir(conf.Server.Backup.Path) - if err != nil { - return 0, fmt.Errorf("unable to read database backup entries: %w", err) - } - - var backupTimes []time.Time - - for _, file := range files { - if !file.IsDir() { - submatch := backupRegex.FindStringSubmatch(file.Name()) - if len(submatch) == 2 { - timestamp, err := time.Parse(backupSuffixLayout, submatch[1]) - if err == nil { - backupTimes = append(backupTimes, timestamp) - } - } - } - } - - if len(backupTimes) <= conf.Server.Backup.Count { - return 0, nil - } - - slices.SortFunc(backupTimes, func(a, b time.Time) int { - return b.Compare(a) - }) - - pruneCount := 0 - var errs []error - - for _, timeToPrune := range backupTimes[conf.Server.Backup.Count:] { - log.Debug(ctx, "Pruning backup", "time", timeToPrune) - path := backupPath(timeToPrune) - err = os.Remove(path) - if err != nil { - errs = append(errs, err) - } else { - pruneCount++ - } - } - - if len(errs) > 0 { - err = errors.Join(errs...) - log.Error(ctx, "Failed to delete one or more files", "errors", err) - } - - return pruneCount, err -} +// +//import ( +// "context" +// "database/sql" +// "errors" +// "fmt" +// "os" +// "path/filepath" +// "regexp" +// "slices" +// "time" +// +// "github.com/mattn/go-sqlite3" +// "github.com/navidrome/navidrome/conf" +// "github.com/navidrome/navidrome/log" +//) +// +//const ( +// backupPrefix = "navidrome_backup" +// backupRegexString = backupPrefix + "_(.+)\\.db" +//) +// +//var backupRegex = regexp.MustCompile(backupRegexString) +// +//const backupSuffixLayout = "2006.01.02_15.04.05" +// +//func backupPath(t time.Time) string { +// return filepath.Join( +// conf.Server.Backup.Path, +// fmt.Sprintf("%s_%s.db", backupPrefix, t.Format(backupSuffixLayout)), +// ) +//} +// +//func backupOrRestore(ctx context.Context, isBackup bool, path string) error { +// // heavily inspired by https://codingrabbits.dev/posts/go_and_sqlite_backup_and_maybe_restore/ +// existingConn, err := Db().Conn(ctx) +// if err != nil { +// return fmt.Errorf("getting existing connection: %w", err) +// } +// defer existingConn.Close() +// +// backupDb, err := sql.Open(Driver, path) +// if err != nil { +// return fmt.Errorf("opening backup database in '%s': %w", path, err) +// } +// defer backupDb.Close() +// +// backupConn, err := backupDb.Conn(ctx) +// if err != nil { +// return fmt.Errorf("getting backup connection: %w", err) +// } +// defer backupConn.Close() +// +// err = existingConn.Raw(func(existing any) error { +// return backupConn.Raw(func(backup any) error { +// var sourceOk, destOk bool +// var sourceConn, destConn *sqlite3.SQLiteConn +// +// if isBackup { +// sourceConn, sourceOk = existing.(*sqlite3.SQLiteConn) +// destConn, destOk = backup.(*sqlite3.SQLiteConn) +// } else { +// sourceConn, sourceOk = backup.(*sqlite3.SQLiteConn) +// destConn, destOk = existing.(*sqlite3.SQLiteConn) +// } +// +// if !sourceOk { +// return fmt.Errorf("error trying to convert source to sqlite connection") +// } +// if !destOk { +// return fmt.Errorf("error trying to convert destination to sqlite connection") +// } +// +// backupOp, err := destConn.Backup("main", sourceConn, "main") +// if err != nil { +// return fmt.Errorf("error starting sqlite backup: %w", err) +// } +// defer backupOp.Close() +// +// // Caution: -1 means that sqlite will hold a read lock until the operation finishes +// // This will lock out other writes that could happen at the same time +// done, err := backupOp.Step(-1) +// if !done { +// return fmt.Errorf("backup not done with step -1") +// } +// if err != nil { +// return fmt.Errorf("error during backup step: %w", err) +// } +// +// err = backupOp.Finish() +// if err != nil { +// return fmt.Errorf("error finishing backup: %w", err) +// } +// +// return nil +// }) +// }) +// +// return err +//} +// +//func Backup(ctx context.Context) (string, error) { +// destPath := backupPath(time.Now()) +// log.Debug(ctx, "Creating backup", "path", destPath) +// err := backupOrRestore(ctx, true, destPath) +// if err != nil { +// return "", err +// } +// +// return destPath, nil +//} +// +//func Restore(ctx context.Context, path string) error { +// log.Debug(ctx, "Restoring backup", "path", path) +// return backupOrRestore(ctx, false, path) +//} +// +//func Prune(ctx context.Context) (int, error) { +// files, err := os.ReadDir(conf.Server.Backup.Path) +// if err != nil { +// return 0, fmt.Errorf("unable to read database backup entries: %w", err) +// } +// +// var backupTimes []time.Time +// +// for _, file := range files { +// if !file.IsDir() { +// submatch := backupRegex.FindStringSubmatch(file.Name()) +// if len(submatch) == 2 { +// timestamp, err := time.Parse(backupSuffixLayout, submatch[1]) +// if err == nil { +// backupTimes = append(backupTimes, timestamp) +// } +// } +// } +// } +// +// if len(backupTimes) <= conf.Server.Backup.Count { +// return 0, nil +// } +// +// slices.SortFunc(backupTimes, func(a, b time.Time) int { +// return b.Compare(a) +// }) +// +// pruneCount := 0 +// var errs []error +// +// for _, timeToPrune := range backupTimes[conf.Server.Backup.Count:] { +// log.Debug(ctx, "Pruning backup", "time", timeToPrune) +// path := backupPath(timeToPrune) +// err = os.Remove(path) +// if err != nil { +// errs = append(errs, err) +// } else { +// pruneCount++ +// } +// } +// +// if len(errs) > 0 { +// err = errors.Join(errs...) +// log.Error(ctx, "Failed to delete one or more files", "errors", err) +// } +// +// return pruneCount, err +//} diff --git a/db/db.go b/db/db.go index cb1ebd9e3..1ebfea561 100644 --- a/db/db.go +++ b/db/db.go @@ -5,20 +5,22 @@ import ( "database/sql" "embed" "fmt" - "runtime" + "path/filepath" + "strings" + "time" - "github.com/mattn/go-sqlite3" + embeddedpostgres "github.com/fergusstrange/embedded-postgres" + _ "github.com/jackc/pgx/v5/stdlib" "github.com/navidrome/navidrome/conf" _ "github.com/navidrome/navidrome/db/migrations" "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/utils/hasher" "github.com/navidrome/navidrome/utils/singleton" "github.com/pressly/goose/v3" ) var ( - Dialect = "sqlite3" - Driver = Dialect + "_custom" + Dialect = "postgres" + Driver = "pgx" Path string ) @@ -27,29 +29,77 @@ var embedMigrations embed.FS const migrationsFolder = "migrations" +var postgresInstance *embeddedpostgres.EmbeddedPostgres + func Db() *sql.DB { return singleton.GetInstance(func() *sql.DB { - sql.Register(Driver, &sqlite3.SQLiteDriver{ - ConnectHook: func(conn *sqlite3.SQLiteConn) error { - return conn.RegisterFunc("SEEDEDRAND", hasher.HashFunc(), false) - }, - }) - Path = conf.Server.DbPath - if Path == ":memory:" { - Path = "file::memory:?cache=shared&_foreign_keys=on" - conf.Server.DbPath = Path + start := time.Now() + log.Info("Starting Embedded Postgres...") + postgresInstance = embeddedpostgres.NewDatabase( + embeddedpostgres. + DefaultConfig(). + Port(5432). + //Password(password). + Logger(&logAdapter{ctx: context.Background()}). + DataPath(filepath.Join(conf.Server.DataFolder, "postgres")). + StartParameters(map[string]string{ + "unix_socket_directories": "/tmp", + "unix_socket_permissions": "0700", + }). + BinariesPath(filepath.Join(conf.Server.CacheFolder, "postgres")), + ) + if err := postgresInstance.Start(); err != nil { + if !strings.Contains(err.Error(), "already listening on port") { + _ = postgresInstance.Stop() + log.Fatal("Failed to start embedded Postgres", err) + } + log.Info("Server already running on port 5432, assuming it's our embedded Postgres", "elapsed", time.Since(start)) + } else { + log.Info("Embedded Postgres started", "elapsed", time.Since(start)) } + + // Create the navidrome database if it doesn't exist + adminPath := "postgresql://postgres:postgres@/postgres?sslmode=disable&host=/tmp" + adminDB, err := sql.Open(Driver, adminPath) + if err != nil { + _ = postgresInstance.Stop() + log.Fatal("Error connecting to admin database", err) + } + defer adminDB.Close() + + // Check if navidrome database exists, create if not + var exists bool + err = adminDB.QueryRow("SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = 'navidrome')").Scan(&exists) + if err != nil { + _ = postgresInstance.Stop() + log.Fatal("Error checking if database exists", err) + } + if !exists { + log.Info("Creating navidrome database...") + _, err = adminDB.Exec("CREATE DATABASE navidrome") + if err != nil { + _ = postgresInstance.Stop() + log.Fatal("Error creating navidrome database", err) + } + } + + // TODO: Implement seeded random function + //sql.Register(Driver, &sqlite3.SQLiteDriver{ + // ConnectHook: func(conn *sqlite3.SQLiteConn) error { + // return conn.RegisterFunc("SEEDEDRAND", hasher.HashFunc(), false) + // }, + //}) + //Path = conf.Server.DbPath + // Ensure client does not attempt TLS when connecting to the embedded Postgres + // and avoid shadowing the package-level Path variable. + Path = "postgresql://postgres:postgres@/navidrome?sslmode=disable&host=/tmp" log.Debug("Opening DataBase", "dbPath", Path, "driver", Driver) db, err := sql.Open(Driver, Path) - db.SetMaxOpenConns(max(4, runtime.NumCPU())) + //db.SetMaxOpenConns(max(4, runtime.NumCPU())) if err != nil { + _ = postgresInstance.Stop() log.Fatal("Error opening database", err) } - _, err = db.Exec("PRAGMA optimize=0x10002") - if err != nil { - log.Error("Error applying PRAGMA optimize", err) - return nil - } return db }) } @@ -58,33 +108,24 @@ func Close(ctx context.Context) { // Ignore cancellations when closing the DB ctx = context.WithoutCancel(ctx) - // Run optimize before closing - Optimize(ctx) - log.Info(ctx, "Closing Database") err := Db().Close() if err != nil { log.Error(ctx, "Error closing Database", err) } + if postgresInstance != nil { + err = postgresInstance.Stop() + if err != nil { + log.Error(ctx, "Error stopping embedded Postgres", err) + } + } } func Init(ctx context.Context) func() { db := Db() - // Disable foreign_keys to allow re-creating tables in migrations - _, err := db.ExecContext(ctx, "PRAGMA foreign_keys=off") - defer func() { - _, err := db.ExecContext(ctx, "PRAGMA foreign_keys=on") - if err != nil { - log.Error(ctx, "Error re-enabling foreign_keys", err) - } - }() - if err != nil { - log.Error(ctx, "Error disabling foreign_keys", err) - } - goose.SetBaseFS(embedMigrations) - err = goose.SetDialect(Dialect) + err := goose.SetDialect(Dialect) if err != nil { log.Fatal(ctx, "Invalid DB driver", "driver", Driver, err) } @@ -99,51 +140,17 @@ func Init(ctx context.Context) func() { log.Fatal(ctx, "Failed to apply new migrations", err) } - if hasSchemaChanges { - log.Debug(ctx, "Applying PRAGMA optimize after schema changes") - _, err = db.ExecContext(ctx, "PRAGMA optimize") - if err != nil { - log.Error(ctx, "Error applying PRAGMA optimize", err) - } - } - return func() { Close(ctx) } } -// Optimize runs PRAGMA optimize on each connection in the pool -func Optimize(ctx context.Context) { - numConns := Db().Stats().OpenConnections - if numConns == 0 { - log.Debug(ctx, "No open connections to optimize") - return - } - log.Debug(ctx, "Optimizing open connections", "numConns", numConns) - var conns []*sql.Conn - for i := 0; i < numConns; i++ { - conn, err := Db().Conn(ctx) - conns = append(conns, conn) - if err != nil { - log.Error(ctx, "Error getting connection from pool", err) - continue - } - _, err = conn.ExecContext(ctx, "PRAGMA optimize;") - if err != nil { - log.Error(ctx, "Error running PRAGMA optimize", err) - } - } - - // Return all connections to the Connection Pool - for _, conn := range conns { - conn.Close() - } -} - type statusLogger struct{ numPending int } func (*statusLogger) Fatalf(format string, v ...interface{}) { log.Fatal(fmt.Sprintf(format, v...)) } func (l *statusLogger) Printf(format string, v ...interface{}) { + // format is part of the goose logger signature; reference it to avoid linter warnings + _ = format if len(v) < 1 { return } @@ -165,11 +172,15 @@ func hasPendingMigrations(ctx context.Context, db *sql.DB, folder string) bool { } func isSchemaEmpty(ctx context.Context, db *sql.DB) bool { - rows, err := db.QueryContext(ctx, "SELECT name FROM sqlite_master WHERE type='table' AND name='goose_db_version';") // nolint:rowserrcheck + rows, err := db.QueryContext(ctx, "SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename = 'goose_db_version';") // nolint:rowserrcheck if err != nil { log.Fatal(ctx, "Database could not be opened!", err) } - defer rows.Close() + defer func() { + if cerr := rows.Close(); cerr != nil { + log.Error(ctx, "Error closing rows", cerr) + } + }() return !rows.Next() } @@ -178,6 +189,11 @@ type logAdapter struct { silent bool } +func (l *logAdapter) Write(p []byte) (n int, err error) { + log.Debug(l.ctx, string(p)) + return len(p), nil +} + func (l *logAdapter) Fatal(v ...interface{}) { log.Fatal(l.ctx, fmt.Sprint(v...)) } diff --git a/db/migrations/20200130083147_create_schema.go b/db/migrations/20200130083147_create_schema.go deleted file mode 100644 index 2fae4f57d..000000000 --- a/db/migrations/20200130083147_create_schema.go +++ /dev/null @@ -1,184 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/navidrome/navidrome/log" - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200130083147, Down20200130083147) -} - -func Up20200130083147(_ context.Context, tx *sql.Tx) error { - log.Info("Creating DB Schema") - _, err := tx.Exec(` -create table if not exists album -( - id varchar(255) not null - primary key, - name varchar(255) default '' not null, - artist_id varchar(255) default '' not null, - cover_art_path varchar(255) default '' not null, - cover_art_id varchar(255) default '' not null, - artist varchar(255) default '' not null, - album_artist varchar(255) default '' not null, - year integer default 0 not null, - compilation bool default FALSE not null, - song_count integer default 0 not null, - duration integer default 0 not null, - genre varchar(255) default '' not null, - created_at datetime, - updated_at datetime -); - -create index if not exists album_artist - on album (artist); - -create index if not exists album_artist_id - on album (artist_id); - -create index if not exists album_genre - on album (genre); - -create index if not exists album_name - on album (name); - -create index if not exists album_year - on album (year); - -create table if not exists annotation -( - ann_id varchar(255) not null - primary key, - user_id varchar(255) default '' not null, - item_id varchar(255) default '' not null, - item_type varchar(255) default '' not null, - play_count integer, - play_date datetime, - rating integer, - starred bool default FALSE not null, - starred_at datetime, - unique (user_id, item_id, item_type) -); - -create index if not exists annotation_play_count - on annotation (play_count); - -create index if not exists annotation_play_date - on annotation (play_date); - -create index if not exists annotation_rating - on annotation (rating); - -create index if not exists annotation_starred - on annotation (starred); - -create table if not exists artist -( - id varchar(255) not null - primary key, - name varchar(255) default '' not null, - album_count integer default 0 not null -); - -create index if not exists artist_name - on artist (name); - -create table if not exists media_file -( - id varchar(255) not null - primary key, - path varchar(255) default '' not null, - title varchar(255) default '' not null, - album varchar(255) default '' not null, - artist varchar(255) default '' not null, - artist_id varchar(255) default '' not null, - album_artist varchar(255) default '' not null, - album_id varchar(255) default '' not null, - has_cover_art bool default FALSE not null, - track_number integer default 0 not null, - disc_number integer default 0 not null, - year integer default 0 not null, - size integer default 0 not null, - suffix varchar(255) default '' not null, - duration integer default 0 not null, - bit_rate integer default 0 not null, - genre varchar(255) default '' not null, - compilation bool default FALSE not null, - created_at datetime, - updated_at datetime -); - -create index if not exists media_file_album_id - on media_file (album_id); - -create index if not exists media_file_genre - on media_file (genre); - -create index if not exists media_file_path - on media_file (path); - -create index if not exists media_file_title - on media_file (title); - -create table if not exists playlist -( - id varchar(255) not null - primary key, - name varchar(255) default '' not null, - comment varchar(255) default '' not null, - duration integer default 0 not null, - owner varchar(255) default '' not null, - public bool default FALSE not null, - tracks text not null -); - -create index if not exists playlist_name - on playlist (name); - -create table if not exists property -( - id varchar(255) not null - primary key, - value varchar(255) default '' not null -); - -create table if not exists search -( - id varchar(255) not null - primary key, - "table" varchar(255) default '' not null, - full_text varchar(255) default '' not null -); - -create index if not exists search_full_text - on search (full_text); - -create index if not exists search_table - on search ("table"); - -create table if not exists user -( - id varchar(255) not null - primary key, - user_name varchar(255) default '' not null - unique, - name varchar(255) default '' not null, - email varchar(255) default '' not null - unique, - password varchar(255) default '' not null, - is_admin bool default FALSE not null, - last_login_at datetime, - last_access_at datetime, - created_at datetime not null, - updated_at datetime not null -);`) - return err -} - -func Down20200130083147(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200131183653_standardize_item_type.go b/db/migrations/20200131183653_standardize_item_type.go deleted file mode 100644 index 471dc8002..000000000 --- a/db/migrations/20200131183653_standardize_item_type.go +++ /dev/null @@ -1,64 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200131183653, Down20200131183653) -} - -func Up20200131183653(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create table search_dg_tmp -( - id varchar(255) not null - primary key, - item_type varchar(255) default '' not null, - full_text varchar(255) default '' not null -); - -insert into search_dg_tmp(id, item_type, full_text) select id, "table", full_text from search; - -drop table search; - -alter table search_dg_tmp rename to search; - -create index search_full_text - on search (full_text); -create index search_table - on search (item_type); - -update annotation set item_type = 'media_file' where item_type = 'mediaFile'; -`) - return err -} - -func Down20200131183653(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create table search_dg_tmp -( - id varchar(255) not null - primary key, - "table" varchar(255) default '' not null, - full_text varchar(255) default '' not null -); - -insert into search_dg_tmp(id, "table", full_text) select id, item_type, full_text from search; - -drop table search; - -alter table search_dg_tmp rename to search; - -create index search_full_text - on search (full_text); -create index search_table - on search ("table"); - -update annotation set item_type = 'mediaFile' where item_type = 'media_file'; -`) - return err -} diff --git a/db/migrations/20200208222418_add_defaults_to_annotations.go b/db/migrations/20200208222418_add_defaults_to_annotations.go deleted file mode 100644 index d058b02c3..000000000 --- a/db/migrations/20200208222418_add_defaults_to_annotations.go +++ /dev/null @@ -1,56 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200208222418, Down20200208222418) -} - -func Up20200208222418(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -update annotation set play_count = 0 where play_count is null; -update annotation set rating = 0 where rating is null; -create table annotation_dg_tmp -( - ann_id varchar(255) not null - primary key, - user_id varchar(255) default '' not null, - item_id varchar(255) default '' not null, - item_type varchar(255) default '' not null, - play_count integer default 0, - play_date datetime, - rating integer default 0, - starred bool default FALSE not null, - starred_at datetime, - unique (user_id, item_id, item_type) -); - -insert into annotation_dg_tmp(ann_id, user_id, item_id, item_type, play_count, play_date, rating, starred, starred_at) select ann_id, user_id, item_id, item_type, play_count, play_date, rating, starred, starred_at from annotation; - -drop table annotation; - -alter table annotation_dg_tmp rename to annotation; - -create index annotation_play_count - on annotation (play_count); - -create index annotation_play_date - on annotation (play_date); - -create index annotation_rating - on annotation (rating); - -create index annotation_starred - on annotation (starred); -`) - return err -} - -func Down20200208222418(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200220143731_change_duration_to_float.go b/db/migrations/20200220143731_change_duration_to_float.go deleted file mode 100644 index 72b785ef8..000000000 --- a/db/migrations/20200220143731_change_duration_to_float.go +++ /dev/null @@ -1,130 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200220143731, Down20200220143731) -} - -func Up20200220143731(_ context.Context, tx *sql.Tx) error { - notice(tx, "This migration will force the next scan to be a full rescan!") - _, err := tx.Exec(` -create table media_file_dg_tmp -( - id varchar(255) not null - primary key, - path varchar(255) default '' not null, - title varchar(255) default '' not null, - album varchar(255) default '' not null, - artist varchar(255) default '' not null, - artist_id varchar(255) default '' not null, - album_artist varchar(255) default '' not null, - album_id varchar(255) default '' not null, - has_cover_art bool default FALSE not null, - track_number integer default 0 not null, - disc_number integer default 0 not null, - year integer default 0 not null, - size integer default 0 not null, - suffix varchar(255) default '' not null, - duration real default 0 not null, - bit_rate integer default 0 not null, - genre varchar(255) default '' not null, - compilation bool default FALSE not null, - created_at datetime, - updated_at datetime -); - -insert into media_file_dg_tmp(id, path, title, album, artist, artist_id, album_artist, album_id, has_cover_art, track_number, disc_number, year, size, suffix, duration, bit_rate, genre, compilation, created_at, updated_at) select id, path, title, album, artist, artist_id, album_artist, album_id, has_cover_art, track_number, disc_number, year, size, suffix, duration, bit_rate, genre, compilation, created_at, updated_at from media_file; - -drop table media_file; - -alter table media_file_dg_tmp rename to media_file; - -create index media_file_album_id - on media_file (album_id); - -create index media_file_genre - on media_file (genre); - -create index media_file_path - on media_file (path); - -create index media_file_title - on media_file (title); - -create table album_dg_tmp -( - id varchar(255) not null - primary key, - name varchar(255) default '' not null, - artist_id varchar(255) default '' not null, - cover_art_path varchar(255) default '' not null, - cover_art_id varchar(255) default '' not null, - artist varchar(255) default '' not null, - album_artist varchar(255) default '' not null, - year integer default 0 not null, - compilation bool default FALSE not null, - song_count integer default 0 not null, - duration real default 0 not null, - genre varchar(255) default '' not null, - created_at datetime, - updated_at datetime -); - -insert into album_dg_tmp(id, name, artist_id, cover_art_path, cover_art_id, artist, album_artist, year, compilation, song_count, duration, genre, created_at, updated_at) select id, name, artist_id, cover_art_path, cover_art_id, artist, album_artist, year, compilation, song_count, duration, genre, created_at, updated_at from album; - -drop table album; - -alter table album_dg_tmp rename to album; - -create index album_artist - on album (artist); - -create index album_artist_id - on album (artist_id); - -create index album_genre - on album (genre); - -create index album_name - on album (name); - -create index album_year - on album (year); - -create table playlist_dg_tmp -( - id varchar(255) not null - primary key, - name varchar(255) default '' not null, - comment varchar(255) default '' not null, - duration real default 0 not null, - owner varchar(255) default '' not null, - public bool default FALSE not null, - tracks text not null -); - -insert into playlist_dg_tmp(id, name, comment, duration, owner, public, tracks) select id, name, comment, duration, owner, public, tracks from playlist; - -drop table playlist; - -alter table playlist_dg_tmp rename to playlist; - -create index playlist_name - on playlist (name); - --- Force a full rescan -delete from property where id like 'LastScan%'; -update media_file set updated_at = '0001-01-01'; -`) - return err -} - -func Down20200220143731(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200310171621_enable_search_by_albumartist.go b/db/migrations/20200310171621_enable_search_by_albumartist.go deleted file mode 100644 index 373e0a475..000000000 --- a/db/migrations/20200310171621_enable_search_by_albumartist.go +++ /dev/null @@ -1,21 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200310171621, Down20200310171621) -} - -func Up20200310171621(_ context.Context, tx *sql.Tx) error { - notice(tx, "A full rescan will be performed to enable search by Album Artist!") - return forceFullRescan(tx) -} - -func Down20200310171621(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200310181627_add_transcoding_and_player_tables.go b/db/migrations/20200310181627_add_transcoding_and_player_tables.go deleted file mode 100644 index 3be91ac35..000000000 --- a/db/migrations/20200310181627_add_transcoding_and_player_tables.go +++ /dev/null @@ -1,54 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200310181627, Down20200310181627) -} - -func Up20200310181627(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create table transcoding -( - id varchar(255) not null primary key, - name varchar(255) not null, - target_format varchar(255) not null, - command varchar(255) default '' not null, - default_bit_rate int default 192, - unique (name), - unique (target_format) -); - -create table player -( - id varchar(255) not null primary key, - name varchar not null, - type varchar, - user_name varchar not null, - client varchar not null, - ip_address varchar, - last_seen timestamp, - max_bit_rate int default 0, - transcoding_id varchar, - unique (name), - foreign key (transcoding_id) - references transcoding(id) - on update restrict - on delete restrict -); -`) - return err -} - -func Down20200310181627(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -drop table transcoding; -drop table player; -`) - return err -} diff --git a/db/migrations/20200319211049_merge_search_into_main_tables.go b/db/migrations/20200319211049_merge_search_into_main_tables.go deleted file mode 100644 index f888cdd4c..000000000 --- a/db/migrations/20200319211049_merge_search_into_main_tables.go +++ /dev/null @@ -1,42 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200319211049, Down20200319211049) -} - -func Up20200319211049(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table media_file - add full_text varchar(255) default ''; -create index if not exists media_file_full_text - on media_file (full_text); - -alter table album - add full_text varchar(255) default ''; -create index if not exists album_full_text - on album (full_text); - -alter table artist - add full_text varchar(255) default ''; -create index if not exists artist_full_text - on artist (full_text); - -drop table if exists search; -`) - if err != nil { - return err - } - notice(tx, "A full rescan will be performed!") - return forceFullRescan(tx) -} - -func Down20200319211049(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200325185135_add_album_artist_id.go b/db/migrations/20200325185135_add_album_artist_id.go deleted file mode 100644 index f01f2c558..000000000 --- a/db/migrations/20200325185135_add_album_artist_id.go +++ /dev/null @@ -1,35 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200325185135, Down20200325185135) -} - -func Up20200325185135(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table album - add album_artist_id varchar(255) default ''; -create index album_artist_album_id - on album (album_artist_id); - -alter table media_file - add album_artist_id varchar(255) default ''; -create index media_file_artist_album_id - on media_file (album_artist_id); -`) - if err != nil { - return err - } - notice(tx, "A full rescan will be performed!") - return forceFullRescan(tx) -} - -func Down20200325185135(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200326090707_fix_album_artists_importing.go b/db/migrations/20200326090707_fix_album_artists_importing.go deleted file mode 100644 index c42e8c327..000000000 --- a/db/migrations/20200326090707_fix_album_artists_importing.go +++ /dev/null @@ -1,21 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200326090707, Down20200326090707) -} - -func Up20200326090707(_ context.Context, tx *sql.Tx) error { - notice(tx, "A full rescan will be performed!") - return forceFullRescan(tx) -} - -func Down20200326090707(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200327193744_add_year_range_to_album.go b/db/migrations/20200327193744_add_year_range_to_album.go deleted file mode 100644 index 66f2b23e8..000000000 --- a/db/migrations/20200327193744_add_year_range_to_album.go +++ /dev/null @@ -1,81 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200327193744, Down20200327193744) -} - -func Up20200327193744(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create table album_dg_tmp -( - id varchar(255) not null - primary key, - name varchar(255) default '' not null, - artist_id varchar(255) default '' not null, - cover_art_path varchar(255) default '' not null, - cover_art_id varchar(255) default '' not null, - artist varchar(255) default '' not null, - album_artist varchar(255) default '' not null, - min_year int default 0 not null, - max_year integer default 0 not null, - compilation bool default FALSE not null, - song_count integer default 0 not null, - duration real default 0 not null, - genre varchar(255) default '' not null, - created_at datetime, - updated_at datetime, - full_text varchar(255) default '', - album_artist_id varchar(255) default '' -); - -insert into album_dg_tmp(id, name, artist_id, cover_art_path, cover_art_id, artist, album_artist, max_year, compilation, song_count, duration, genre, created_at, updated_at, full_text, album_artist_id) select id, name, artist_id, cover_art_path, cover_art_id, artist, album_artist, year, compilation, song_count, duration, genre, created_at, updated_at, full_text, album_artist_id from album; - -drop table album; - -alter table album_dg_tmp rename to album; - -create index album_artist - on album (artist); - -create index album_artist_album - on album (artist); - -create index album_artist_album_id - on album (album_artist_id); - -create index album_artist_id - on album (artist_id); - -create index album_full_text - on album (full_text); - -create index album_genre - on album (genre); - -create index album_name - on album (name); - -create index album_min_year - on album (min_year); - -create index album_max_year - on album (max_year); - -`) - if err != nil { - return err - } - notice(tx, "A full rescan will be performed!") - return forceFullRescan(tx) -} - -func Down20200327193744(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200404214704_add_indexes.go b/db/migrations/20200404214704_add_indexes.go deleted file mode 100644 index 6207b0a3d..000000000 --- a/db/migrations/20200404214704_add_indexes.go +++ /dev/null @@ -1,30 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200404214704, Down20200404214704) -} - -func Up20200404214704(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create index if not exists media_file_year - on media_file (year); - -create index if not exists media_file_duration - on media_file (duration); - -create index if not exists media_file_track_number - on media_file (disc_number, track_number); -`) - return err -} - -func Down20200404214704(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200409002249_enable_search_by_tracks_artists.go b/db/migrations/20200409002249_enable_search_by_tracks_artists.go deleted file mode 100644 index 22006c8af..000000000 --- a/db/migrations/20200409002249_enable_search_by_tracks_artists.go +++ /dev/null @@ -1,21 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200409002249, Down20200409002249) -} - -func Up20200409002249(_ context.Context, tx *sql.Tx) error { - notice(tx, "A full rescan will be performed to enable search by individual Artist in an Album!") - return forceFullRescan(tx) -} - -func Down20200409002249(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200411164603_add_created_and_updated_fields_to_playlists.go b/db/migrations/20200411164603_add_created_and_updated_fields_to_playlists.go deleted file mode 100644 index 266dc087d..000000000 --- a/db/migrations/20200411164603_add_created_and_updated_fields_to_playlists.go +++ /dev/null @@ -1,28 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200411164603, Down20200411164603) -} - -func Up20200411164603(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table playlist - add created_at datetime; -alter table playlist - add updated_at datetime; -update playlist - set created_at = datetime('now'), updated_at = datetime('now'); -`) - return err -} - -func Down20200411164603(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200418110522_reindex_to_fix_album_years.go b/db/migrations/20200418110522_reindex_to_fix_album_years.go deleted file mode 100644 index 22b024cea..000000000 --- a/db/migrations/20200418110522_reindex_to_fix_album_years.go +++ /dev/null @@ -1,21 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200418110522, Down20200418110522) -} - -func Up20200418110522(_ context.Context, tx *sql.Tx) error { - notice(tx, "A full rescan will be performed to fix search Albums by year") - return forceFullRescan(tx) -} - -func Down20200418110522(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200419222708_reindex_to_change_full_text_search.go b/db/migrations/20200419222708_reindex_to_change_full_text_search.go deleted file mode 100644 index efeb1bb84..000000000 --- a/db/migrations/20200419222708_reindex_to_change_full_text_search.go +++ /dev/null @@ -1,21 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200419222708, Down20200419222708) -} - -func Up20200419222708(_ context.Context, tx *sql.Tx) error { - notice(tx, "A full rescan will be performed to change the search behaviour") - return forceFullRescan(tx) -} - -func Down20200419222708(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200423204116_add_sort_fields.go b/db/migrations/20200423204116_add_sort_fields.go deleted file mode 100644 index 4097a9d60..000000000 --- a/db/migrations/20200423204116_add_sort_fields.go +++ /dev/null @@ -1,66 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200423204116, Down20200423204116) -} - -func Up20200423204116(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table artist - add order_artist_name varchar(255) collate nocase; -alter table artist - add sort_artist_name varchar(255) collate nocase; -create index if not exists artist_order_artist_name - on artist (order_artist_name); - -alter table album - add order_album_name varchar(255) collate nocase; -alter table album - add order_album_artist_name varchar(255) collate nocase; -alter table album - add sort_album_name varchar(255) collate nocase; -alter table album - add sort_artist_name varchar(255) collate nocase; -alter table album - add sort_album_artist_name varchar(255) collate nocase; -create index if not exists album_order_album_name - on album (order_album_name); -create index if not exists album_order_album_artist_name - on album (order_album_artist_name); - -alter table media_file - add order_album_name varchar(255) collate nocase; -alter table media_file - add order_album_artist_name varchar(255) collate nocase; -alter table media_file - add order_artist_name varchar(255) collate nocase; -alter table media_file - add sort_album_name varchar(255) collate nocase; -alter table media_file - add sort_artist_name varchar(255) collate nocase; -alter table media_file - add sort_album_artist_name varchar(255) collate nocase; -alter table media_file - add sort_title varchar(255) collate nocase; -create index if not exists media_file_order_album_name - on media_file (order_album_name); -create index if not exists media_file_order_artist_name - on media_file (order_artist_name); -`) - if err != nil { - return err - } - notice(tx, "A full rescan will be performed to change the search behaviour") - return forceFullRescan(tx) -} - -func Down20200423204116(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200508093059_add_artist_song_count.go b/db/migrations/20200508093059_add_artist_song_count.go deleted file mode 100644 index aac78e698..000000000 --- a/db/migrations/20200508093059_add_artist_song_count.go +++ /dev/null @@ -1,28 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200508093059, Down20200508093059) -} - -func Up20200508093059(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table artist - add song_count integer default 0 not null; -`) - if err != nil { - return err - } - notice(tx, "A full rescan will be performed to calculate artists' song counts") - return forceFullRescan(tx) -} - -func Down20200508093059(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200512104202_add_disc_subtitle.go b/db/migrations/20200512104202_add_disc_subtitle.go deleted file mode 100644 index b3e907d8d..000000000 --- a/db/migrations/20200512104202_add_disc_subtitle.go +++ /dev/null @@ -1,28 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200512104202, Down20200512104202) -} - -func Up20200512104202(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table media_file - add disc_subtitle varchar(255); - `) - if err != nil { - return err - } - notice(tx, "A full rescan will be performed to import disc subtitles") - return forceFullRescan(tx) -} - -func Down20200512104202(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200516140647_add_playlist_tracks_table.go b/db/migrations/20200516140647_add_playlist_tracks_table.go deleted file mode 100644 index fcaae9d8e..000000000 --- a/db/migrations/20200516140647_add_playlist_tracks_table.go +++ /dev/null @@ -1,101 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - "strings" - - "github.com/navidrome/navidrome/log" - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200516140647, Down20200516140647) -} - -func Up20200516140647(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create table if not exists playlist_tracks -( - id integer default 0 not null, - playlist_id varchar(255) not null, - media_file_id varchar(255) not null -); - -create unique index if not exists playlist_tracks_pos - on playlist_tracks (playlist_id, id); -`) - if err != nil { - return err - } - rows, err := tx.Query("select id, tracks from playlist") - if err != nil { - return err - } - defer rows.Close() - var id, tracks string - for rows.Next() { - err := rows.Scan(&id, &tracks) - if err != nil { - return err - } - err = Up20200516140647UpdatePlaylistTracks(tx, id, tracks) - if err != nil { - return err - } - } - err = rows.Err() - if err != nil { - return err - } - - _, err = tx.Exec(` -create table playlist_dg_tmp -( - id varchar(255) not null - primary key, - name varchar(255) default '' not null, - comment varchar(255) default '' not null, - duration real default 0 not null, - song_count integer default 0 not null, - owner varchar(255) default '' not null, - public bool default FALSE not null, - created_at datetime, - updated_at datetime -); - -insert into playlist_dg_tmp(id, name, comment, duration, owner, public, created_at, updated_at) - select id, name, comment, duration, owner, public, created_at, updated_at from playlist; - -drop table playlist; - -alter table playlist_dg_tmp rename to playlist; - -create index playlist_name - on playlist (name); - -update playlist set song_count = (select count(*) from playlist_tracks where playlist_id = playlist.id) -where id <> '' - -`) - return err -} - -func Up20200516140647UpdatePlaylistTracks(tx *sql.Tx, id string, tracks string) error { - trackList := strings.Split(tracks, ",") - stmt, err := tx.Prepare("insert into playlist_tracks (playlist_id, media_file_id, id) values (?, ?, ?)") - if err != nil { - return err - } - for i, trackId := range trackList { - _, err := stmt.Exec(id, trackId, i+1) - if err != nil { - log.Error("Error adding track to playlist", "playlistId", id, "trackId", trackId, err) - } - } - return nil -} - -func Down20200516140647(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200608153717_referential_integrity.go b/db/migrations/20200608153717_referential_integrity.go deleted file mode 100644 index 2959237fa..000000000 --- a/db/migrations/20200608153717_referential_integrity.go +++ /dev/null @@ -1,138 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20200608153717, Down20200608153717) -} - -func Up20200608153717(_ context.Context, tx *sql.Tx) error { - // First delete dangling players - _, err := tx.Exec(` -delete from player where user_name not in (select user_name from user)`) - if err != nil { - return err - } - - // Also delete dangling players - _, err = tx.Exec(` -delete from playlist where owner not in (select user_name from user)`) - if err != nil { - return err - } - - // Also delete dangling playlist tracks - _, err = tx.Exec(` -delete from playlist_tracks where playlist_id not in (select id from playlist)`) - if err != nil { - return err - } - - // Add foreign key to player table - err = updatePlayer_20200608153717(tx) - if err != nil { - return err - } - - // Add foreign key to playlist table - err = updatePlaylist_20200608153717(tx) - if err != nil { - return err - } - - // Add foreign keys to playlist_tracks table - return updatePlaylistTracks_20200608153717(tx) -} - -func updatePlayer_20200608153717(tx *sql.Tx) error { - _, err := tx.Exec(` -create table player_dg_tmp -( - id varchar(255) not null - primary key, - name varchar not null - unique, - type varchar, - user_name varchar not null - references user (user_name) - on update cascade on delete cascade, - client varchar not null, - ip_address varchar, - last_seen timestamp, - max_bit_rate int default 0, - transcoding_id varchar null -); - -insert into player_dg_tmp(id, name, type, user_name, client, ip_address, last_seen, max_bit_rate, transcoding_id) select id, name, type, user_name, client, ip_address, last_seen, max_bit_rate, transcoding_id from player; - -drop table player; - -alter table player_dg_tmp rename to player; -`) - return err -} - -func updatePlaylist_20200608153717(tx *sql.Tx) error { - _, err := tx.Exec(` -create table playlist_dg_tmp -( - id varchar(255) not null - primary key, - name varchar(255) default '' not null, - comment varchar(255) default '' not null, - duration real default 0 not null, - song_count integer default 0 not null, - owner varchar(255) default '' not null - constraint playlist_user_user_name_fk - references user (user_name) - on update cascade on delete cascade, - public bool default FALSE not null, - created_at datetime, - updated_at datetime -); - -insert into playlist_dg_tmp(id, name, comment, duration, song_count, owner, public, created_at, updated_at) select id, name, comment, duration, song_count, owner, public, created_at, updated_at from playlist; - -drop table playlist; - -alter table playlist_dg_tmp rename to playlist; - -create index playlist_name - on playlist (name); -`) - return err -} - -func updatePlaylistTracks_20200608153717(tx *sql.Tx) error { - _, err := tx.Exec(` -create table playlist_tracks_dg_tmp -( - id integer default 0 not null, - playlist_id varchar(255) not null - constraint playlist_tracks_playlist_id_fk - references playlist - on update cascade on delete cascade, - media_file_id varchar(255) not null -); - -insert into playlist_tracks_dg_tmp(id, playlist_id, media_file_id) select id, playlist_id, media_file_id from playlist_tracks; - -drop table playlist_tracks; - -alter table playlist_tracks_dg_tmp rename to playlist_tracks; - -create unique index playlist_tracks_pos - on playlist_tracks (playlist_id, id); - -`) - return err -} - -func Down20200608153717(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200706231659_add_default_transcodings.go b/db/migrations/20200706231659_add_default_transcodings.go deleted file mode 100644 index a498d32b0..000000000 --- a/db/migrations/20200706231659_add_default_transcodings.go +++ /dev/null @@ -1,43 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/navidrome/navidrome/consts" - "github.com/navidrome/navidrome/model/id" - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddDefaultTranscodings, downAddDefaultTranscodings) -} - -func upAddDefaultTranscodings(_ context.Context, tx *sql.Tx) error { - row := tx.QueryRow("SELECT COUNT(*) FROM transcoding") - var count int - err := row.Scan(&count) - if err != nil { - return err - } - if count > 0 { - return nil - } - - stmt, err := tx.Prepare("insert into transcoding (id, name, target_format, default_bit_rate, command) values (?, ?, ?, ?, ?)") - if err != nil { - return err - } - - for _, t := range consts.DefaultTranscodings { - _, err := stmt.Exec(id.NewRandom(), t.Name, t.TargetFormat, t.DefaultBitRate, t.Command) - if err != nil { - return err - } - } - return nil -} - -func downAddDefaultTranscodings(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200710211442_add_playlist_path.go b/db/migrations/20200710211442_add_playlist_path.go deleted file mode 100644 index 8abfed6cf..000000000 --- a/db/migrations/20200710211442_add_playlist_path.go +++ /dev/null @@ -1,28 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddPlaylistPath, downAddPlaylistPath) -} - -func upAddPlaylistPath(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table playlist - add path string default '' not null; - -alter table playlist - add sync bool default false not null; -`) - - return err -} - -func downAddPlaylistPath(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200731095603_create_play_queues_table.go b/db/migrations/20200731095603_create_play_queues_table.go deleted file mode 100644 index d63a1ecb9..000000000 --- a/db/migrations/20200731095603_create_play_queues_table.go +++ /dev/null @@ -1,37 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upCreatePlayQueuesTable, downCreatePlayQueuesTable) -} - -func upCreatePlayQueuesTable(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create table playqueue -( - id varchar(255) not null primary key, - user_id varchar(255) not null - references user (id) - on update cascade on delete cascade, - comment varchar(255), - current varchar(255) not null, - position integer, - changed_by varchar(255), - items varchar(255), - created_at datetime, - updated_at datetime -); -`) - - return err -} - -func downCreatePlayQueuesTable(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200801101355_create_bookmark_table.go b/db/migrations/20200801101355_create_bookmark_table.go deleted file mode 100644 index fe68fafd7..000000000 --- a/db/migrations/20200801101355_create_bookmark_table.go +++ /dev/null @@ -1,54 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upCreateBookmarkTable, downCreateBookmarkTable) -} - -func upCreateBookmarkTable(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create table bookmark -( - user_id varchar(255) not null - references user - on update cascade on delete cascade, - item_id varchar(255) not null, - item_type varchar(255) not null, - comment varchar(255), - position integer, - changed_by varchar(255), - created_at datetime, - updated_at datetime, - constraint bookmark_pk - unique (user_id, item_id, item_type) -); - -create table playqueue_dg_tmp -( - id varchar(255) not null, - user_id varchar(255) not null - references user - on update cascade on delete cascade, - current varchar(255), - position real, - changed_by varchar(255), - items varchar(255), - created_at datetime, - updated_at datetime -); -drop table playqueue; -alter table playqueue_dg_tmp rename to playqueue; -`) - - return err -} - -func downCreateBookmarkTable(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20200819111809_drop_email_unique_constraint.go b/db/migrations/20200819111809_drop_email_unique_constraint.go deleted file mode 100644 index b2dd4285c..000000000 --- a/db/migrations/20200819111809_drop_email_unique_constraint.go +++ /dev/null @@ -1,43 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upDropEmailUniqueConstraint, downDropEmailUniqueConstraint) -} - -func upDropEmailUniqueConstraint(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create table user_dg_tmp -( - id varchar(255) not null - primary key, - user_name varchar(255) default '' not null - unique, - name varchar(255) default '' not null, - email varchar(255) default '' not null, - password varchar(255) default '' not null, - is_admin bool default FALSE not null, - last_login_at datetime, - last_access_at datetime, - created_at datetime not null, - updated_at datetime not null -); - -insert into user_dg_tmp(id, user_name, name, email, password, is_admin, last_login_at, last_access_at, created_at, updated_at) select id, user_name, name, email, password, is_admin, last_login_at, last_access_at, created_at, updated_at from user; - -drop table user; - -alter table user_dg_tmp rename to user; -`) - return err -} - -func downDropEmailUniqueConstraint(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20201003111749_add_starred_at_index.go b/db/migrations/20201003111749_add_starred_at_index.go deleted file mode 100644 index 7ee7a283f..000000000 --- a/db/migrations/20201003111749_add_starred_at_index.go +++ /dev/null @@ -1,24 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20201003111749, Down20201003111749) -} - -func Up20201003111749(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create index if not exists annotation_starred_at - on annotation (starred_at); - `) - return err -} - -func Down20201003111749(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20201010162350_add_album_size.go b/db/migrations/20201010162350_add_album_size.go deleted file mode 100644 index f1182ab6c..000000000 --- a/db/migrations/20201010162350_add_album_size.go +++ /dev/null @@ -1,34 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20201010162350, Down20201010162350) -} - -func Up20201010162350(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table album - add size integer default 0 not null; -create index if not exists album_size - on album(size); - -update album set size = ifnull(( -select sum(f.size) -from media_file f -where f.album_id = album.id -), 0) -where id not null;`) - - return err -} - -func Down20201010162350(_ context.Context, tx *sql.Tx) error { - // This code is executed when the migration is rolled back. - return nil -} diff --git a/db/migrations/20201012210022_add_artist_playlist_size.go b/db/migrations/20201012210022_add_artist_playlist_size.go deleted file mode 100644 index 4eb67f14e..000000000 --- a/db/migrations/20201012210022_add_artist_playlist_size.go +++ /dev/null @@ -1,45 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20201012210022, Down20201012210022) -} - -func Up20201012210022(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table artist - add size integer default 0 not null; -create index if not exists artist_size - on artist(size); - -update artist set size = ifnull(( - select sum(f.size) - from album f - where f.album_artist_id = artist.id -), 0) -where id not null; - -alter table playlist - add size integer default 0 not null; -create index if not exists playlist_size - on playlist(size); - -update playlist set size = ifnull(( - select sum(size) - from media_file f - left join playlist_tracks pt on f.id = pt.media_file_id - where pt.playlist_id = playlist.id -), 0);`) - - return err -} - -func Down20201012210022(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20201021085410_add_mbids.go b/db/migrations/20201021085410_add_mbids.go deleted file mode 100644 index 624bb1a67..000000000 --- a/db/migrations/20201021085410_add_mbids.go +++ /dev/null @@ -1,59 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20201021085410, Down20201021085410) -} - -func Up20201021085410(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table media_file - add mbz_track_id varchar(255); -alter table media_file - add mbz_album_id varchar(255); -alter table media_file - add mbz_artist_id varchar(255); -alter table media_file - add mbz_album_artist_id varchar(255); -alter table media_file - add mbz_album_type varchar(255); -alter table media_file - add mbz_album_comment varchar(255); -alter table media_file - add catalog_num varchar(255); - -alter table album - add mbz_album_id varchar(255); -alter table album - add mbz_album_artist_id varchar(255); -alter table album - add mbz_album_type varchar(255); -alter table album - add mbz_album_comment varchar(255); -alter table album - add catalog_num varchar(255); - -create index if not exists album_mbz_album_type - on album (mbz_album_type); - -alter table artist - add mbz_artist_id varchar(255); - -`) - if err != nil { - return err - } - notice(tx, "A full rescan needs to be performed to import more tags") - return forceFullRescan(tx) -} - -func Down20201021085410(_ context.Context, tx *sql.Tx) error { - // This code is executed when the migration is rolled back. - return nil -} diff --git a/db/migrations/20201021093209_add_media_file_indexes.go b/db/migrations/20201021093209_add_media_file_indexes.go deleted file mode 100644 index f3a800949..000000000 --- a/db/migrations/20201021093209_add_media_file_indexes.go +++ /dev/null @@ -1,28 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20201021093209, Down20201021093209) -} - -func Up20201021093209(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create index if not exists media_file_artist - on media_file (artist); -create index if not exists media_file_album_artist - on media_file (album_artist); -create index if not exists media_file_mbz_track_id - on media_file (mbz_track_id); -`) - return err -} - -func Down20201021093209(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20201021135455_add_media_file_artist_index.go b/db/migrations/20201021135455_add_media_file_artist_index.go deleted file mode 100644 index ca04d8a20..000000000 --- a/db/migrations/20201021135455_add_media_file_artist_index.go +++ /dev/null @@ -1,24 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20201021135455, Down20201021135455) -} - -func Up20201021135455(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create index if not exists media_file_artist_id - on media_file (artist_id); -`) - return err -} - -func Down20201021135455(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20201030162009_add_artist_info_table.go b/db/migrations/20201030162009_add_artist_info_table.go deleted file mode 100644 index f2917ae49..000000000 --- a/db/migrations/20201030162009_add_artist_info_table.go +++ /dev/null @@ -1,36 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddArtistImageUrl, downAddArtistImageUrl) -} - -func upAddArtistImageUrl(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table artist - add biography varchar(255) default '' not null; -alter table artist - add small_image_url varchar(255) default '' not null; -alter table artist - add medium_image_url varchar(255) default '' not null; -alter table artist - add large_image_url varchar(255) default '' not null; -alter table artist - add similar_artists varchar(255) default '' not null; -alter table artist - add external_url varchar(255) default '' not null; -alter table artist - add external_info_updated_at datetime; -`) - return err -} - -func downAddArtistImageUrl(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20201110205344_add_comments_and_lyrics.go b/db/migrations/20201110205344_add_comments_and_lyrics.go deleted file mode 100644 index 5bb17b8d0..000000000 --- a/db/migrations/20201110205344_add_comments_and_lyrics.go +++ /dev/null @@ -1,33 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20201110205344, Down20201110205344) -} - -func Up20201110205344(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table media_file - add comment varchar; -alter table media_file - add lyrics varchar; - -alter table album - add comment varchar; -`) - if err != nil { - return err - } - notice(tx, "A full rescan will be performed to import comments and lyrics") - return forceFullRescan(tx) -} - -func Down20201110205344(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20201128100726_add_real-path_option.go b/db/migrations/20201128100726_add_real-path_option.go deleted file mode 100644 index db102dfa9..000000000 --- a/db/migrations/20201128100726_add_real-path_option.go +++ /dev/null @@ -1,24 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20201128100726, Down20201128100726) -} - -func Up20201128100726(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table player - add report_real_path bool default FALSE not null; -`) - return err -} - -func Down20201128100726(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20201213124814_add_all_artist_ids_to_album.go b/db/migrations/20201213124814_add_all_artist_ids_to_album.go deleted file mode 100644 index 170497f5c..000000000 --- a/db/migrations/20201213124814_add_all_artist_ids_to_album.go +++ /dev/null @@ -1,64 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/utils/str" - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20201213124814, Down20201213124814) -} - -func Up20201213124814(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table album - add all_artist_ids varchar; - -create index if not exists album_all_artist_ids - on album (all_artist_ids); -`) - if err != nil { - return err - } - - return updateAlbums20201213124814(tx) -} - -func updateAlbums20201213124814(tx *sql.Tx) error { - rows, err := tx.Query(` -select a.id, a.name, a.artist_id, a.album_artist_id, group_concat(mf.artist_id, ' ') - from album a left join media_file mf on a.id = mf.album_id group by a.id - `) - if err != nil { - return err - } - defer rows.Close() - - stmt, err := tx.Prepare("update album set all_artist_ids = ? where id = ?") - if err != nil { - return err - } - - var id, name, artistId, albumArtistId string - var songArtistIds sql.NullString - for rows.Next() { - err = rows.Scan(&id, &name, &artistId, &albumArtistId, &songArtistIds) - if err != nil { - return err - } - all := str.SanitizeStrings(artistId, albumArtistId, songArtistIds.String) - _, err = stmt.Exec(all, id) - if err != nil { - log.Error("Error setting album's artist_ids", "album", name, "albumId", id, err) - } - } - return rows.Err() -} - -func Down20201213124814(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20210322132848_add_timestamp_indexes.go b/db/migrations/20210322132848_add_timestamp_indexes.go deleted file mode 100644 index 3341dd3d2..000000000 --- a/db/migrations/20210322132848_add_timestamp_indexes.go +++ /dev/null @@ -1,34 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddTimestampIndexesGo, downAddTimestampIndexesGo) -} - -func upAddTimestampIndexesGo(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create index if not exists album_updated_at - on album (updated_at); -create index if not exists album_created_at - on album (created_at); -create index if not exists playlist_updated_at - on playlist (updated_at); -create index if not exists playlist_created_at - on playlist (created_at); -create index if not exists media_file_created_at - on media_file (created_at); -create index if not exists media_file_updated_at - on media_file (updated_at); -`) - return err -} - -func downAddTimestampIndexesGo(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20210418232815_fix_album_comments.go b/db/migrations/20210418232815_fix_album_comments.go deleted file mode 100644 index 59067640a..000000000 --- a/db/migrations/20210418232815_fix_album_comments.go +++ /dev/null @@ -1,68 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - "strings" - - "github.com/navidrome/navidrome/consts" - "github.com/navidrome/navidrome/log" - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upFixAlbumComments, downFixAlbumComments) -} - -func upFixAlbumComments(_ context.Context, tx *sql.Tx) error { - //nolint:gosec - rows, err := tx.Query(` - SELECT album.id, group_concat(media_file.comment, '` + consts.Zwsp + `') FROM album, media_file WHERE media_file.album_id = album.id GROUP BY album.id; - `) - if err != nil { - return err - } - defer rows.Close() - - stmt, err := tx.Prepare("UPDATE album SET comment = ? WHERE id = ?") - if err != nil { - return err - } - var id string - var comments sql.NullString - - for rows.Next() { - err = rows.Scan(&id, &comments) - if err != nil { - return err - } - if !comments.Valid { - continue - } - comment := getComment(comments.String, consts.Zwsp) - _, err = stmt.Exec(comment, id) - - if err != nil { - log.Error("Error setting album's comments", "albumId", id, err) - } - } - return rows.Err() -} - -func downFixAlbumComments(_ context.Context, tx *sql.Tx) error { - return nil -} - -func getComment(comments string, separator string) string { - cs := strings.Split(comments, separator) - if len(cs) == 0 { - return "" - } - first := cs[0] - for _, c := range cs[1:] { - if first != c { - return "" - } - } - return first -} diff --git a/db/migrations/20210430212322_add_bpm_metadata.go b/db/migrations/20210430212322_add_bpm_metadata.go deleted file mode 100644 index 721c9e179..000000000 --- a/db/migrations/20210430212322_add_bpm_metadata.go +++ /dev/null @@ -1,31 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddBpmMetadata, downAddBpmMetadata) -} - -func upAddBpmMetadata(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table media_file - add bpm integer; - -create index if not exists media_file_bpm - on media_file (bpm); -`) - if err != nil { - return err - } - notice(tx, "A full rescan needs to be performed to import more tags") - return forceFullRescan(tx) -} - -func downAddBpmMetadata(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20210530121921_create_shares_table.go b/db/migrations/20210530121921_create_shares_table.go deleted file mode 100644 index e9208bd69..000000000 --- a/db/migrations/20210530121921_create_shares_table.go +++ /dev/null @@ -1,35 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upCreateSharesTable, downCreateSharesTable) -} - -func upCreateSharesTable(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create table share -( - id varchar(255) not null primary key, - name varchar(255) not null unique, - description varchar(255), - expires datetime, - created datetime, - last_visited datetime, - resource_ids varchar not null, - resource_type varchar(255) not null, - visit_count integer default 0 -); -`) - - return err -} - -func downCreateSharesTable(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20210601231734_update_share_fieldnames.go b/db/migrations/20210601231734_update_share_fieldnames.go deleted file mode 100644 index 965c0186e..000000000 --- a/db/migrations/20210601231734_update_share_fieldnames.go +++ /dev/null @@ -1,26 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upUpdateShareFieldNames, downUpdateShareFieldNames) -} - -func upUpdateShareFieldNames(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table share rename column expires to expires_at; -alter table share rename column created to created_at; -alter table share rename column last_visited to last_visited_at; -`) - - return err -} - -func downUpdateShareFieldNames(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20210616150710_encrypt_all_passwords.go b/db/migrations/20210616150710_encrypt_all_passwords.go deleted file mode 100644 index f67e3fb0a..000000000 --- a/db/migrations/20210616150710_encrypt_all_passwords.go +++ /dev/null @@ -1,56 +0,0 @@ -package migrations - -import ( - "context" - "crypto/sha256" - "database/sql" - - "github.com/navidrome/navidrome/consts" - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/utils" - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upEncodeAllPasswords, downEncodeAllPasswords) -} - -func upEncodeAllPasswords(ctx context.Context, tx *sql.Tx) error { - rows, err := tx.Query(`SELECT id, user_name, password from user;`) - if err != nil { - return err - } - defer rows.Close() - - stmt, err := tx.Prepare("UPDATE user SET password = ? WHERE id = ?") - if err != nil { - return err - } - var id string - var username, password string - - data := sha256.Sum256([]byte(consts.DefaultEncryptionKey)) - encKey := data[0:] - - for rows.Next() { - err = rows.Scan(&id, &username, &password) - if err != nil { - return err - } - - password, err = utils.Encrypt(ctx, encKey, password) - if err != nil { - log.Error("Error encrypting user's password", "id", id, "username", username, err) - } - - _, err = stmt.Exec(password, id) - if err != nil { - log.Error("Error saving user's encrypted password", "id", id, "username", username, err) - } - } - return rows.Err() -} - -func downEncodeAllPasswords(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20210619231716_drop_player_name_unique_constraint.go b/db/migrations/20210619231716_drop_player_name_unique_constraint.go deleted file mode 100644 index 200332156..000000000 --- a/db/migrations/20210619231716_drop_player_name_unique_constraint.go +++ /dev/null @@ -1,48 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upDropPlayerNameUniqueConstraint, downDropPlayerNameUniqueConstraint) -} - -func upDropPlayerNameUniqueConstraint(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create table player_dg_tmp -( - id varchar(255) not null - primary key, - name varchar not null, - user_agent varchar, - user_name varchar not null - references user (user_name) - on update cascade on delete cascade, - client varchar not null, - ip_address varchar, - last_seen timestamp, - max_bit_rate int default 0, - transcoding_id varchar, - report_real_path bool default FALSE not null -); - -insert into player_dg_tmp(id, name, user_agent, user_name, client, ip_address, last_seen, max_bit_rate, transcoding_id, report_real_path) select id, name, type, user_name, client, ip_address, last_seen, max_bit_rate, transcoding_id, report_real_path from player; - -drop table player; - -alter table player_dg_tmp rename to player; -create index if not exists player_match - on player (client, user_agent, user_name); -create index if not exists player_name - on player (name); -`) - return err -} - -func downDropPlayerNameUniqueConstraint(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20210623155401_add_user_prefs_player_scrobbler_enabled.go b/db/migrations/20210623155401_add_user_prefs_player_scrobbler_enabled.go deleted file mode 100644 index 5257dfab3..000000000 --- a/db/migrations/20210623155401_add_user_prefs_player_scrobbler_enabled.go +++ /dev/null @@ -1,45 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddUserPrefsPlayerScrobblerEnabled, downAddUserPrefsPlayerScrobblerEnabled) -} - -func upAddUserPrefsPlayerScrobblerEnabled(_ context.Context, tx *sql.Tx) error { - err := upAddUserPrefs(tx) - if err != nil { - return err - } - return upPlayerScrobblerEnabled(tx) -} - -func upAddUserPrefs(tx *sql.Tx) error { - _, err := tx.Exec(` -create table user_props -( - user_id varchar not null, - key varchar not null, - value varchar, - constraint user_props_pk - primary key (user_id, key) -); -`) - return err -} - -func upPlayerScrobblerEnabled(tx *sql.Tx) error { - _, err := tx.Exec(` -alter table player add scrobble_enabled bool default true; -`) - return err -} - -func downAddUserPrefsPlayerScrobblerEnabled(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20210625223901_add_referential_integrity_to_user_props.go b/db/migrations/20210625223901_add_referential_integrity_to_user_props.go deleted file mode 100644 index 033392d93..000000000 --- a/db/migrations/20210625223901_add_referential_integrity_to_user_props.go +++ /dev/null @@ -1,39 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddReferentialIntegrityToUserProps, downAddReferentialIntegrityToUserProps) -} - -func upAddReferentialIntegrityToUserProps(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create table user_props_dg_tmp -( - user_id varchar not null - constraint user_props_user_id_fk - references user - on update cascade on delete cascade, - key varchar not null, - value varchar, - constraint user_props_pk - primary key (user_id, key) -); - -insert into user_props_dg_tmp(user_id, key, value) select user_id, key, value from user_props; - -drop table user_props; - -alter table user_props_dg_tmp rename to user_props; -`) - return err -} - -func downAddReferentialIntegrityToUserProps(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20210626213026_add_scrobble_buffer.go b/db/migrations/20210626213026_add_scrobble_buffer.go deleted file mode 100644 index 1c4d0de2a..000000000 --- a/db/migrations/20210626213026_add_scrobble_buffer.go +++ /dev/null @@ -1,39 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddScrobbleBuffer, downAddScrobbleBuffer) -} - -func upAddScrobbleBuffer(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create table if not exists scrobble_buffer -( - user_id varchar not null - constraint scrobble_buffer_user_id_fk - references user - on update cascade on delete cascade, - service varchar not null, - media_file_id varchar not null - constraint scrobble_buffer_media_file_id_fk - references media_file - on update cascade on delete cascade, - play_time datetime not null, - enqueue_time datetime not null default current_timestamp, - constraint scrobble_buffer_pk - unique (user_id, service, media_file_id, play_time, user_id) -); -`) - - return err -} - -func downAddScrobbleBuffer(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20210715151153_add_genre_tables.go b/db/migrations/20210715151153_add_genre_tables.go deleted file mode 100644 index ab2c54239..000000000 --- a/db/migrations/20210715151153_add_genre_tables.go +++ /dev/null @@ -1,69 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddGenreTables, downAddGenreTables) -} - -func upAddGenreTables(_ context.Context, tx *sql.Tx) error { - notice(tx, "A full rescan will be performed to import multiple genres!") - _, err := tx.Exec(` -create table if not exists genre -( - id varchar not null primary key, - name varchar not null, - constraint genre_name_ux - unique (name) -); - -create table if not exists album_genres -( - album_id varchar default null not null - references album - on delete cascade, - genre_id varchar default null not null - references genre - on delete cascade, - constraint album_genre_ux - unique (album_id, genre_id) -); - -create table if not exists media_file_genres -( - media_file_id varchar default null not null - references media_file - on delete cascade, - genre_id varchar default null not null - references genre - on delete cascade, - constraint media_file_genre_ux - unique (media_file_id, genre_id) -); - -create table if not exists artist_genres -( - artist_id varchar default null not null - references artist - on delete cascade, - genre_id varchar default null not null - references genre - on delete cascade, - constraint artist_genre_ux - unique (artist_id, genre_id) -); -`) - if err != nil { - return err - } - return forceFullRescan(tx) -} - -func downAddGenreTables(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20210821212604_add_mediafile_channels.go b/db/migrations/20210821212604_add_mediafile_channels.go deleted file mode 100644 index 9a0988b17..000000000 --- a/db/migrations/20210821212604_add_mediafile_channels.go +++ /dev/null @@ -1,31 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddMediafileChannels, downAddMediafileChannels) -} - -func upAddMediafileChannels(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table media_file - add channels integer; - -create index if not exists media_file_channels - on media_file (channels); -`) - if err != nil { - return err - } - notice(tx, "A full rescan needs to be performed to import more tags") - return forceFullRescan(tx) -} - -func downAddMediafileChannels(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20211008205505_add_smart_playlist.go b/db/migrations/20211008205505_add_smart_playlist.go deleted file mode 100644 index c8ed67c47..000000000 --- a/db/migrations/20211008205505_add_smart_playlist.go +++ /dev/null @@ -1,38 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddSmartPlaylist, downAddSmartPlaylist) -} - -func upAddSmartPlaylist(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table playlist - add column rules varchar null; -alter table playlist - add column evaluated_at datetime null; -create index if not exists playlist_evaluated_at - on playlist(evaluated_at); - -create table playlist_fields ( - field varchar(255) not null, - playlist_id varchar(255) not null - constraint playlist_fields_playlist_id_fk - references playlist - on update cascade on delete cascade -); -create unique index playlist_fields_idx - on playlist_fields (field, playlist_id); -`) - return err -} - -func downAddSmartPlaylist(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20211023184825_add_order_title_to_media_file.go b/db/migrations/20211023184825_add_order_title_to_media_file.go deleted file mode 100644 index ee6fc67d1..000000000 --- a/db/migrations/20211023184825_add_order_title_to_media_file.go +++ /dev/null @@ -1,62 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - "strings" - - "github.com/deluan/sanitize" - "github.com/navidrome/navidrome/log" - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddOrderTitleToMediaFile, downAddOrderTitleToMediaFile) -} - -func upAddOrderTitleToMediaFile(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table main.media_file - add order_title varchar null collate NOCASE; -create index if not exists media_file_order_title - on media_file (order_title); -`) - if err != nil { - return err - } - - return upAddOrderTitleToMediaFile_populateOrderTitle(tx) -} - -//goland:noinspection GoSnakeCaseUsage -func upAddOrderTitleToMediaFile_populateOrderTitle(tx *sql.Tx) error { - rows, err := tx.Query(`select id, title from media_file`) - if err != nil { - return err - } - defer rows.Close() - - stmt, err := tx.Prepare("update media_file set order_title = ? where id = ?") - if err != nil { - return err - } - - var id, title string - for rows.Next() { - err = rows.Scan(&id, &title) - if err != nil { - return err - } - - orderTitle := strings.TrimSpace(sanitize.Accents(title)) - _, err = stmt.Exec(orderTitle, id) - if err != nil { - log.Error("Error setting media_file's order_title", "title", title, "id", id, err) - } - } - return rows.Err() -} - -func downAddOrderTitleToMediaFile(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20211026191915_unescape_lyrics_and_comments.go b/db/migrations/20211026191915_unescape_lyrics_and_comments.go deleted file mode 100644 index d4ba5e194..000000000 --- a/db/migrations/20211026191915_unescape_lyrics_and_comments.go +++ /dev/null @@ -1,48 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/utils/str" - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upUnescapeLyricsAndComments, downUnescapeLyricsAndComments) -} - -func upUnescapeLyricsAndComments(_ context.Context, tx *sql.Tx) error { - rows, err := tx.Query(`select id, comment, lyrics, title from media_file`) - if err != nil { - return err - } - defer rows.Close() - - stmt, err := tx.Prepare("update media_file set comment = ?, lyrics = ? where id = ?") - if err != nil { - return err - } - - var id, title string - var comment, lyrics sql.NullString - for rows.Next() { - err = rows.Scan(&id, &comment, &lyrics, &title) - if err != nil { - return err - } - - newComment := str.SanitizeText(comment.String) - newLyrics := str.SanitizeText(lyrics.String) - _, err = stmt.Exec(newComment, newLyrics, id) - if err != nil { - log.Error("Error unescaping media_file's lyrics and comments", "title", title, "id", id, err) - } - } - return rows.Err() -} - -func downUnescapeLyricsAndComments(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20211029213200_add_userid_to_playlist.go b/db/migrations/20211029213200_add_userid_to_playlist.go deleted file mode 100644 index e262fc205..000000000 --- a/db/migrations/20211029213200_add_userid_to_playlist.go +++ /dev/null @@ -1,61 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddUseridToPlaylist, downAddUseridToPlaylist) -} - -func upAddUseridToPlaylist(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create table playlist_dg_tmp -( - id varchar(255) not null - primary key, - name varchar(255) default '' not null, - comment varchar(255) default '' not null, - duration real default 0 not null, - song_count integer default 0 not null, - public bool default FALSE not null, - created_at datetime, - updated_at datetime, - path string default '' not null, - sync bool default false not null, - size integer default 0 not null, - rules varchar, - evaluated_at datetime, - owner_id varchar(255) not null - constraint playlist_user_user_id_fk - references user - on update cascade on delete cascade -); - -insert into playlist_dg_tmp(id, name, comment, duration, song_count, public, created_at, updated_at, path, sync, size, rules, evaluated_at, owner_id) -select id, name, comment, duration, song_count, public, created_at, updated_at, path, sync, size, rules, evaluated_at, - (select id from user where user_name = owner) as user_id from playlist; - -drop table playlist; -alter table playlist_dg_tmp rename to playlist; -create index playlist_created_at - on playlist (created_at); -create index playlist_evaluated_at - on playlist (evaluated_at); -create index playlist_name - on playlist (name); -create index playlist_size - on playlist (size); -create index playlist_updated_at - on playlist (updated_at); - -`) - return err -} - -func downAddUseridToPlaylist(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20211102215414_add_alphabetical_by_artist_index.go b/db/migrations/20211102215414_add_alphabetical_by_artist_index.go deleted file mode 100644 index 4ab4305d0..000000000 --- a/db/migrations/20211102215414_add_alphabetical_by_artist_index.go +++ /dev/null @@ -1,24 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddAlphabeticalByArtistIndex, downAddAlphabeticalByArtistIndex) -} - -func upAddAlphabeticalByArtistIndex(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create index album_alphabetical_by_artist - ON album(compilation, order_album_artist_name, order_album_name) -`) - return err -} - -func downAddAlphabeticalByArtistIndex(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20211105162746_remove_invalid_artist_ids.go b/db/migrations/20211105162746_remove_invalid_artist_ids.go deleted file mode 100644 index 5e078c820..000000000 --- a/db/migrations/20211105162746_remove_invalid_artist_ids.go +++ /dev/null @@ -1,23 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upRemoveInvalidArtistIds, downRemoveInvalidArtistIds) -} - -func upRemoveInvalidArtistIds(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -update media_file set artist_id = '' where not exists(select 1 from artist where id = artist_id) -`) - return err -} - -func downRemoveInvalidArtistIds(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20220724231849_add_musicbrainz_release_track_id.go b/db/migrations/20220724231849_add_musicbrainz_release_track_id.go deleted file mode 100644 index 481762117..000000000 --- a/db/migrations/20220724231849_add_musicbrainz_release_track_id.go +++ /dev/null @@ -1,29 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddMusicbrainzReleaseTrackId, downAddMusicbrainzReleaseTrackId) -} - -func upAddMusicbrainzReleaseTrackId(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table media_file - add mbz_release_track_id varchar(255); -`) - if err != nil { - return err - } - notice(tx, "A full rescan needs to be performed to import more tags") - return forceFullRescan(tx) -} - -func downAddMusicbrainzReleaseTrackId(_ context.Context, tx *sql.Tx) error { - // This code is executed when the migration is rolled back. - return nil -} diff --git a/db/migrations/20221219112733_add_album_image_paths.go b/db/migrations/20221219112733_add_album_image_paths.go deleted file mode 100644 index ee9c77c8a..000000000 --- a/db/migrations/20221219112733_add_album_image_paths.go +++ /dev/null @@ -1,27 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddAlbumImagePaths, downAddAlbumImagePaths) -} - -func upAddAlbumImagePaths(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table main.album add image_files varchar; -`) - if err != nil { - return err - } - notice(tx, "A full rescan needs to be performed to import all album images") - return forceFullRescan(tx) -} - -func downAddAlbumImagePaths(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20221219140528_remove_cover_art_id.go b/db/migrations/20221219140528_remove_cover_art_id.go deleted file mode 100644 index a1eaa89f9..000000000 --- a/db/migrations/20221219140528_remove_cover_art_id.go +++ /dev/null @@ -1,28 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upRemoveCoverArtId, downRemoveCoverArtId) -} - -func upRemoveCoverArtId(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table album drop column cover_art_id; -alter table album rename column cover_art_path to embed_art_path -`) - if err != nil { - return err - } - notice(tx, "A full rescan needs to be performed to import all album images") - return forceFullRescan(tx) -} - -func downRemoveCoverArtId(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20230112111457_add_album_paths.go b/db/migrations/20230112111457_add_album_paths.go deleted file mode 100644 index 2dfb9a747..000000000 --- a/db/migrations/20230112111457_add_album_paths.go +++ /dev/null @@ -1,68 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - "path/filepath" - "slices" - "strings" - - "github.com/navidrome/navidrome/consts" - "github.com/navidrome/navidrome/log" - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddAlbumPaths, downAddAlbumPaths) -} - -func upAddAlbumPaths(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(`alter table album add paths varchar;`) - if err != nil { - return err - } - - //nolint:gosec - rows, err := tx.Query(` - select album_id, group_concat(path, '` + consts.Zwsp + `') from media_file group by album_id - `) - if err != nil { - return err - } - - stmt, err := tx.Prepare("update album set paths = ? where id = ?") - if err != nil { - return err - } - - var id, filePaths string - for rows.Next() { - err = rows.Scan(&id, &filePaths) - if err != nil { - return err - } - - paths := upAddAlbumPathsDirs(filePaths) - _, err = stmt.Exec(paths, id) - if err != nil { - log.Error("Error updating album's paths", "paths", paths, "id", id, err) - } - } - return rows.Err() -} - -func upAddAlbumPathsDirs(filePaths string) string { - allPaths := strings.Split(filePaths, consts.Zwsp) - var dirs []string - for _, p := range allPaths { - dir, _ := filepath.Split(p) - dirs = append(dirs, filepath.Clean(dir)) - } - slices.Sort(dirs) - dirs = slices.Compact(dirs) - return strings.Join(dirs, string(filepath.ListSeparator)) -} - -func downAddAlbumPaths(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20230114121537_touch_playlists.go b/db/migrations/20230114121537_touch_playlists.go deleted file mode 100644 index 0f10e275c..000000000 --- a/db/migrations/20230114121537_touch_playlists.go +++ /dev/null @@ -1,21 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upTouchPlaylists, downTouchPlaylists) -} - -func upTouchPlaylists(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(`update playlist set updated_at = datetime('now');`) - return err -} - -func downTouchPlaylists(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20230115103212_create_internet_radio.go b/db/migrations/20230115103212_create_internet_radio.go deleted file mode 100644 index 5c014dac2..000000000 --- a/db/migrations/20230115103212_create_internet_radio.go +++ /dev/null @@ -1,31 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upCreateInternetRadio, downCreateInternetRadio) -} - -func upCreateInternetRadio(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -create table if not exists radio -( - id varchar(255) not null primary key, - name varchar not null unique, - stream_url varchar not null, - home_page_url varchar default '' not null, - created_at datetime, - updated_at datetime -); -`) - return err -} - -func downCreateInternetRadio(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20230117155559_add_replaygain_metadata.go b/db/migrations/20230117155559_add_replaygain_metadata.go deleted file mode 100644 index d6be3b313..000000000 --- a/db/migrations/20230117155559_add_replaygain_metadata.go +++ /dev/null @@ -1,35 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddReplaygainMetadata, downAddReplaygainMetadata) -} - -func upAddReplaygainMetadata(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table media_file add - rg_album_gain real; -alter table media_file add - rg_album_peak real; -alter table media_file add - rg_track_gain real; -alter table media_file add - rg_track_peak real; -`) - if err != nil { - return err - } - - notice(tx, "A full rescan needs to be performed to import more tags") - return forceFullRescan(tx) -} - -func downAddReplaygainMetadata(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20230117180400_add_album_info.go b/db/migrations/20230117180400_add_album_info.go deleted file mode 100644 index 5d6dd8230..000000000 --- a/db/migrations/20230117180400_add_album_info.go +++ /dev/null @@ -1,34 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddAlbumInfo, downAddAlbumInfo) -} - -func upAddAlbumInfo(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table album - add description varchar(255) default '' not null; -alter table album - add small_image_url varchar(255) default '' not null; -alter table album - add medium_image_url varchar(255) default '' not null; -alter table album - add large_image_url varchar(255) default '' not null; -alter table album - add external_url varchar(255) default '' not null; -alter table album - add external_info_updated_at datetime; -`) - return err -} - -func downAddAlbumInfo(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20230119152657_recreate_share_table.go b/db/migrations/20230119152657_recreate_share_table.go deleted file mode 100644 index e1ae816c0..000000000 --- a/db/migrations/20230119152657_recreate_share_table.go +++ /dev/null @@ -1,42 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddMissingShareInfo, downAddMissingShareInfo) -} - -func upAddMissingShareInfo(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -drop table if exists share; -create table share -( - id varchar(255) not null - primary key, - description varchar(255), - expires_at datetime, - last_visited_at datetime, - resource_ids varchar not null, - resource_type varchar(255) not null, - contents varchar, - format varchar, - max_bit_rate integer, - visit_count integer default 0, - created_at datetime, - updated_at datetime, - user_id varchar(255) not null - constraint share_user_id_fk - references user -); -`) - return err -} - -func downAddMissingShareInfo(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20230202143713_change_path_list_separator.go b/db/migrations/20230202143713_change_path_list_separator.go deleted file mode 100644 index 78b030ae4..000000000 --- a/db/migrations/20230202143713_change_path_list_separator.go +++ /dev/null @@ -1,63 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - "path/filepath" - "slices" - "strings" - - "github.com/navidrome/navidrome/consts" - "github.com/navidrome/navidrome/log" - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upChangePathListSeparator, downChangePathListSeparator) -} - -func upChangePathListSeparator(_ context.Context, tx *sql.Tx) error { - //nolint:gosec - rows, err := tx.Query(` - select album_id, group_concat(path, '` + consts.Zwsp + `') from media_file group by album_id - `) - if err != nil { - return err - } - - stmt, err := tx.Prepare("update album set paths = ? where id = ?") - if err != nil { - return err - } - - var id, filePaths string - for rows.Next() { - err = rows.Scan(&id, &filePaths) - if err != nil { - return err - } - - paths := upChangePathListSeparatorDirs(filePaths) - _, err = stmt.Exec(paths, id) - if err != nil { - log.Error("Error updating album's paths", "paths", paths, "id", id, err) - } - } - return rows.Err() -} - -func upChangePathListSeparatorDirs(filePaths string) string { - allPaths := strings.Split(filePaths, consts.Zwsp) - var dirs []string - for _, p := range allPaths { - dir, _ := filepath.Split(p) - dirs = append(dirs, filepath.Clean(dir)) - } - slices.Sort(dirs) - dirs = slices.Compact(dirs) - return strings.Join(dirs, consts.Zwsp) -} - -func downChangePathListSeparator(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20230209181414_change_image_files_list_separator.go b/db/migrations/20230209181414_change_image_files_list_separator.go deleted file mode 100644 index 7f4d4cb0e..000000000 --- a/db/migrations/20230209181414_change_image_files_list_separator.go +++ /dev/null @@ -1,60 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - "path/filepath" - "slices" - "strings" - - "github.com/navidrome/navidrome/consts" - "github.com/navidrome/navidrome/log" - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upChangeImageFilesListSeparator, downChangeImageFilesListSeparator) -} - -func upChangeImageFilesListSeparator(_ context.Context, tx *sql.Tx) error { - rows, err := tx.Query(`select id, image_files from album`) - if err != nil { - return err - } - - stmt, err := tx.Prepare("update album set image_files = ? where id = ?") - if err != nil { - return err - } - - var id string - var imageFiles sql.NullString - for rows.Next() { - err = rows.Scan(&id, &imageFiles) - if err != nil { - return err - } - - files := upChangeImageFilesListSeparatorDirs(imageFiles.String) - if files == imageFiles.String { - continue - } - _, err = stmt.Exec(files, id) - if err != nil { - log.Error("Error updating album's image file list", "files", files, "id", id, err) - } - } - return rows.Err() -} - -func upChangeImageFilesListSeparatorDirs(filePaths string) string { - allPaths := filepath.SplitList(filePaths) - slices.Sort(allPaths) - allPaths = slices.Compact(allPaths) - return strings.Join(allPaths, consts.Zwsp) -} - -func downChangeImageFilesListSeparator(_ context.Context, tx *sql.Tx) error { - // This code is executed when the migration is rolled back. - return nil -} diff --git a/db/migrations/20230310222612_add_download_to_share.go b/db/migrations/20230310222612_add_download_to_share.go deleted file mode 100644 index ed2879ec3..000000000 --- a/db/migrations/20230310222612_add_download_to_share.go +++ /dev/null @@ -1,24 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddDownloadToShare, downAddDownloadToShare) -} - -func upAddDownloadToShare(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table share - add downloadable bool not null default false; -`) - return err -} - -func downAddDownloadToShare(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20230404104309_empty_sql_migration.sql b/db/migrations/20230404104309_empty_sql_migration.sql deleted file mode 100644 index 7a2c92116..000000000 --- a/db/migrations/20230404104309_empty_sql_migration.sql +++ /dev/null @@ -1,13 +0,0 @@ --- This file has intentionally no SQL logic. It is here to avoid an error in the linter: --- db/db.go:23:4: invalid go:embed: build system did not supply embed configuration (typecheck) --- - --- +goose Up --- +goose StatementBegin -SELECT 'up SQL query'; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -SELECT 'down SQL query'; --- +goose StatementEnd diff --git a/db/migrations/20230515184510_add_release_date.go b/db/migrations/20230515184510_add_release_date.go deleted file mode 100644 index 1141a1e74..000000000 --- a/db/migrations/20230515184510_add_release_date.go +++ /dev/null @@ -1,50 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddRelRecYear, downAddRelRecYear) -} - -func upAddRelRecYear(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table media_file - add date varchar(255) default '' not null; -alter table media_file - add original_year int default 0 not null; -alter table media_file - add original_date varchar(255) default '' not null; -alter table media_file - add release_year int default 0 not null; -alter table media_file - add release_date varchar(255) default '' not null; - -alter table album - add date varchar(255) default '' not null; -alter table album - add min_original_year int default 0 not null; -alter table album - add max_original_year int default 0 not null; -alter table album - add original_date varchar(255) default '' not null; -alter table album - add release_date varchar(255) default '' not null; -alter table album - add releases integer default 0 not null; -`) - if err != nil { - return err - } - - notice(tx, "A full rescan needs to be performed to import more tags") - return forceFullRescan(tx) -} - -func downAddRelRecYear(_ context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20230616214944_rename_musicbrainz_recording_id.go b/db/migrations/20230616214944_rename_musicbrainz_recording_id.go deleted file mode 100644 index 170fc264c..000000000 --- a/db/migrations/20230616214944_rename_musicbrainz_recording_id.go +++ /dev/null @@ -1,28 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upRenameMusicbrainzRecordingId, downRenameMusicbrainzRecordingId) -} - -func upRenameMusicbrainzRecordingId(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table media_file - rename column mbz_track_id to mbz_recording_id; -`) - return err -} - -func downRenameMusicbrainzRecordingId(_ context.Context, tx *sql.Tx) error { - _, err := tx.Exec(` -alter table media_file - rename column mbz_recording_id to mbz_track_id; -`) - return err -} diff --git a/db/migrations/20231208182311_add_discs_to_album.go b/db/migrations/20231208182311_add_discs_to_album.go deleted file mode 100644 index e4bf9a778..000000000 --- a/db/migrations/20231208182311_add_discs_to_album.go +++ /dev/null @@ -1,36 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddDiscToAlbum, downAddDiscToAlbum) -} - -func upAddDiscToAlbum(ctx context.Context, tx *sql.Tx) error { - _, err := tx.ExecContext(ctx, `alter table album add discs JSONB default '{}';`) - if err != nil { - return err - } - _, err = tx.ExecContext(ctx, ` -update album set discs = t.discs -from (select album_id, json_group_object(disc_number, disc_subtitle) as discs - from (select distinct album_id, disc_number, disc_subtitle - from media_file - where disc_number > 0 - order by album_id, disc_number) - group by album_id - having discs <> '{"1":""}') as t -where album.id = t.album_id; -`) - return err -} - -func downAddDiscToAlbum(ctx context.Context, tx *sql.Tx) error { - _, err := tx.ExecContext(ctx, `alter table album drop discs;`) - return err -} diff --git a/db/migrations/20231209211223_alter_lyric_column.go b/db/migrations/20231209211223_alter_lyric_column.go deleted file mode 100644 index ac73fc98f..000000000 --- a/db/migrations/20231209211223_alter_lyric_column.go +++ /dev/null @@ -1,82 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - "encoding/json" - - "github.com/navidrome/navidrome/model" - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAlterLyricColumn, downAlterLyricColumn) -} - -func upAlterLyricColumn(ctx context.Context, tx *sql.Tx) error { - _, err := tx.ExecContext(ctx, `alter table media_file rename COLUMN lyrics TO lyrics_old`) - if err != nil { - return err - } - - _, err = tx.ExecContext(ctx, `alter table media_file add lyrics JSONB default '[]';`) - if err != nil { - return err - } - - stmt, err := tx.Prepare(`update media_file SET lyrics = ? where id = ?`) - if err != nil { - return err - } - - rows, err := tx.Query(`select id, lyrics_old FROM media_file WHERE lyrics_old <> '';`) - if err != nil { - return err - } - - var id string - var lyrics sql.NullString - for rows.Next() { - err = rows.Scan(&id, &lyrics) - if err != nil { - return err - } - - if !lyrics.Valid { - continue - } - - lyrics, err := model.ToLyrics("xxx", lyrics.String) - if err != nil { - return err - } - - text, err := json.Marshal(model.LyricList{*lyrics}) - if err != nil { - return err - } - - _, err = stmt.Exec(string(text), id) - if err != nil { - return err - } - } - - err = rows.Err() - if err != nil { - return err - } - - _, err = tx.ExecContext(ctx, `ALTER TABLE media_file DROP COLUMN lyrics_old;`) - if err != nil { - return err - } - - notice(tx, "A full rescan should be performed to pick up additional lyrics (existing lyrics have been preserved)") - return nil -} - -func downAlterLyricColumn(ctx context.Context, tx *sql.Tx) error { - // This code is executed when the migration is rolled back. - return nil -} diff --git a/db/migrations/20240122223340_add_default_values_to_null_columns.go.go b/db/migrations/20240122223340_add_default_values_to_null_columns.go.go deleted file mode 100644 index a65b0aefd..000000000 --- a/db/migrations/20240122223340_add_default_values_to_null_columns.go.go +++ /dev/null @@ -1,563 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(Up20240122223340, Down20240122223340) -} - -func Up20240122223340(ctx context.Context, tx *sql.Tx) error { - _, err := tx.ExecContext(ctx, ` -drop index if exists album_alphabetical_by_artist; -drop index if exists album_order_album_name; -drop index if exists album_order_album_artist_name; -drop index if exists album_mbz_album_type; - -drop index if exists artist_order_artist_name; - -drop index if exists media_file_order_album_name; -drop index if exists media_file_order_artist_name; -drop index if exists media_file_order_title; -drop index if exists media_file_bpm; -drop index if exists media_file_channels; -drop index if exists media_file_mbz_track_id; - -alter table album - add image_files_new varchar not null default ''; -update album -set image_files_new = image_files -where image_files is not null; -alter table album - drop image_files; -alter table album - rename image_files_new to image_files; - -alter table album - add order_album_name_new varchar not null default ''; -update album -set order_album_name_new = order_album_name -where order_album_name is not null; -alter table album - drop order_album_name; -alter table album - rename order_album_name_new to order_album_name; - -alter table album - add order_album_artist_name_new varchar not null default ''; -update album -set order_album_artist_name_new = order_album_artist_name -where order_album_artist_name is not null; -alter table album - drop order_album_artist_name; -alter table album - rename order_album_artist_name_new to order_album_artist_name; - -alter table album - add sort_album_name_new varchar not null default ''; -update album -set sort_album_name_new = sort_album_name -where sort_album_name is not null; -alter table album - drop sort_album_name; -alter table album - rename sort_album_name_new to sort_album_name; - -alter table album - add sort_artist_name_new varchar not null default ''; -update album -set sort_artist_name_new = sort_artist_name -where sort_artist_name is not null; -alter table album - drop sort_artist_name; -alter table album - rename sort_artist_name_new to sort_artist_name; - -alter table album - add sort_album_artist_name_new varchar not null default ''; -update album -set sort_album_artist_name_new = sort_album_artist_name -where sort_album_artist_name is not null; -alter table album - drop sort_album_artist_name; -alter table album - rename sort_album_artist_name_new to sort_album_artist_name; - -alter table album - add catalog_num_new varchar not null default ''; -update album -set catalog_num_new = catalog_num -where catalog_num is not null; -alter table album - drop catalog_num; -alter table album - rename catalog_num_new to catalog_num; - -alter table album - add comment_new varchar not null default ''; -update album -set comment_new = comment -where comment is not null; -alter table album - drop comment; -alter table album - rename comment_new to comment; - -alter table album - add paths_new varchar not null default ''; -update album -set paths_new = paths -where paths is not null; -alter table album - drop paths; -alter table album - rename paths_new to paths; - -alter table album - add mbz_album_id_new varchar not null default ''; -update album -set mbz_album_id_new = mbz_album_id -where mbz_album_id is not null; -alter table album - drop mbz_album_id; -alter table album - rename mbz_album_id_new to mbz_album_id; - -alter table album - add mbz_album_artist_id_new varchar not null default ''; -update album -set mbz_album_artist_id_new = mbz_album_artist_id -where mbz_album_artist_id is not null; -alter table album - drop mbz_album_artist_id; -alter table album - rename mbz_album_artist_id_new to mbz_album_artist_id; - -alter table album - add mbz_album_type_new varchar not null default ''; -update album -set mbz_album_type_new = mbz_album_type -where mbz_album_type is not null; -alter table album - drop mbz_album_type; -alter table album - rename mbz_album_type_new to mbz_album_type; - -alter table album - add mbz_album_comment_new varchar not null default ''; -update album -set mbz_album_comment_new = mbz_album_comment -where mbz_album_comment is not null; -alter table album - drop mbz_album_comment; -alter table album - rename mbz_album_comment_new to mbz_album_comment; - -alter table album - add discs_new jsonb not null default '{}'; -update album -set discs_new = discs -where discs is not null; -alter table album - drop discs; -alter table album - rename discs_new to discs; - --- ARTIST -alter table artist - add order_artist_name_new varchar not null default ''; -update artist -set order_artist_name_new = order_artist_name -where order_artist_name is not null; -alter table artist - drop order_artist_name; -alter table artist - rename order_artist_name_new to order_artist_name; - -alter table artist - add sort_artist_name_new varchar not null default ''; -update artist -set sort_artist_name_new = sort_artist_name -where sort_artist_name is not null; -alter table artist - drop sort_artist_name; -alter table artist - rename sort_artist_name_new to sort_artist_name; - -alter table artist - add mbz_artist_id_new varchar not null default ''; -update artist -set mbz_artist_id_new = mbz_artist_id -where mbz_artist_id is not null; -alter table artist - drop mbz_artist_id; -alter table artist - rename mbz_artist_id_new to mbz_artist_id; - --- MEDIA_FILE -alter table media_file - add order_album_name_new varchar not null default ''; -update media_file -set order_album_name_new = order_album_name -where order_album_name is not null; -alter table media_file - drop order_album_name; -alter table media_file - rename order_album_name_new to order_album_name; - -alter table media_file - add order_album_artist_name_new varchar not null default ''; -update media_file -set order_album_artist_name_new = order_album_artist_name -where order_album_artist_name is not null; -alter table media_file - drop order_album_artist_name; -alter table media_file - rename order_album_artist_name_new to order_album_artist_name; - -alter table media_file - add order_artist_name_new varchar not null default ''; -update media_file -set order_artist_name_new = order_artist_name -where order_artist_name is not null; -alter table media_file - drop order_artist_name; -alter table media_file - rename order_artist_name_new to order_artist_name; - -alter table media_file - add sort_album_name_new varchar not null default ''; -update media_file -set sort_album_name_new = sort_album_name -where sort_album_name is not null; -alter table media_file - drop sort_album_name; -alter table media_file - rename sort_album_name_new to sort_album_name; - -alter table media_file - add sort_artist_name_new varchar not null default ''; -update media_file -set sort_artist_name_new = sort_artist_name -where sort_artist_name is not null; -alter table media_file - drop sort_artist_name; -alter table media_file - rename sort_artist_name_new to sort_artist_name; - -alter table media_file - add sort_album_artist_name_new varchar not null default ''; -update media_file -set sort_album_artist_name_new = sort_album_artist_name -where sort_album_artist_name is not null; -alter table media_file - drop sort_album_artist_name; -alter table media_file - rename sort_album_artist_name_new to sort_album_artist_name; - -alter table media_file - add sort_title_new varchar not null default ''; -update media_file -set sort_title_new = sort_title -where sort_title is not null; -alter table media_file - drop sort_title; -alter table media_file - rename sort_title_new to sort_title; - -alter table media_file - add disc_subtitle_new varchar not null default ''; -update media_file -set disc_subtitle_new = disc_subtitle -where disc_subtitle is not null; -alter table media_file - drop disc_subtitle; -alter table media_file - rename disc_subtitle_new to disc_subtitle; - -alter table media_file - add catalog_num_new varchar not null default ''; -update media_file -set catalog_num_new = catalog_num -where catalog_num is not null; -alter table media_file - drop catalog_num; -alter table media_file - rename catalog_num_new to catalog_num; - -alter table media_file - add comment_new varchar not null default ''; -update media_file -set comment_new = comment -where comment is not null; -alter table media_file - drop comment; -alter table media_file - rename comment_new to comment; - -alter table media_file - add order_title_new varchar not null default ''; -update media_file -set order_title_new = order_title -where order_title is not null; -alter table media_file - drop order_title; -alter table media_file - rename order_title_new to order_title; - -alter table media_file - add mbz_recording_id_new varchar not null default ''; -update media_file -set mbz_recording_id_new = mbz_recording_id -where mbz_recording_id is not null; -alter table media_file - drop mbz_recording_id; -alter table media_file - rename mbz_recording_id_new to mbz_recording_id; - -alter table media_file - add mbz_album_id_new varchar not null default ''; -update media_file -set mbz_album_id_new = mbz_album_id -where mbz_album_id is not null; -alter table media_file - drop mbz_album_id; -alter table media_file - rename mbz_album_id_new to mbz_album_id; - -alter table media_file - add mbz_artist_id_new varchar not null default ''; -update media_file -set mbz_artist_id_new = mbz_artist_id -where mbz_artist_id is not null; -alter table media_file - drop mbz_artist_id; -alter table media_file - rename mbz_artist_id_new to mbz_artist_id; - -alter table media_file - add mbz_artist_id_new varchar not null default ''; -update media_file -set mbz_artist_id_new = mbz_artist_id -where mbz_artist_id is not null; -alter table media_file - drop mbz_artist_id; -alter table media_file - rename mbz_artist_id_new to mbz_artist_id; - -alter table media_file - add mbz_album_artist_id_new varchar not null default ''; -update media_file -set mbz_album_artist_id_new = mbz_album_artist_id -where mbz_album_artist_id is not null; -alter table media_file - drop mbz_album_artist_id; -alter table media_file - rename mbz_album_artist_id_new to mbz_album_artist_id; - -alter table media_file - add mbz_album_type_new varchar not null default ''; -update media_file -set mbz_album_type_new = mbz_album_type -where mbz_album_type is not null; -alter table media_file - drop mbz_album_type; -alter table media_file - rename mbz_album_type_new to mbz_album_type; - -alter table media_file - add mbz_album_comment_new varchar not null default ''; -update media_file -set mbz_album_comment_new = mbz_album_comment -where mbz_album_comment is not null; -alter table media_file - drop mbz_album_comment; -alter table media_file - rename mbz_album_comment_new to mbz_album_comment; - -alter table media_file - add mbz_release_track_id_new varchar not null default ''; -update media_file -set mbz_release_track_id_new = mbz_release_track_id -where mbz_release_track_id is not null; -alter table media_file - drop mbz_release_track_id; -alter table media_file - rename mbz_release_track_id_new to mbz_release_track_id; - -alter table media_file - add bpm_new integer not null default 0; -update media_file -set bpm_new = bpm -where bpm is not null; -alter table media_file - drop bpm; -alter table media_file - rename bpm_new to bpm; - -alter table media_file - add channels_new integer not null default 0; -update media_file -set channels_new = channels -where channels is not null; -alter table media_file - drop channels; -alter table media_file - rename channels_new to channels; - -alter table media_file - add rg_album_gain_new real not null default 0; -update media_file -set rg_album_gain_new = rg_album_gain -where rg_album_gain is not null; -alter table media_file - drop rg_album_gain; -alter table media_file - rename rg_album_gain_new to rg_album_gain; - -alter table media_file - add rg_album_peak_new real not null default 0; -update media_file -set rg_album_peak_new = rg_album_peak -where rg_album_peak is not null; -alter table media_file - drop rg_album_peak; -alter table media_file - rename rg_album_peak_new to rg_album_peak; - -alter table media_file - add rg_track_gain_new real not null default 0; -update media_file -set rg_track_gain_new = rg_track_gain -where rg_track_gain is not null; -alter table media_file - drop rg_track_gain; -alter table media_file - rename rg_track_gain_new to rg_track_gain; - -alter table media_file - add rg_track_peak_new real not null default 0; -update media_file -set rg_track_peak_new = rg_track_peak -where rg_track_peak is not null; -alter table media_file - drop rg_track_peak; -alter table media_file - rename rg_track_peak_new to rg_track_peak; - -alter table media_file - add lyrics_new jsonb not null default '[]'; -update media_file -set lyrics_new = lyrics -where lyrics is not null; -alter table media_file - drop lyrics; -alter table media_file - rename lyrics_new to lyrics; - --- SHARE -alter table share - add description_new varchar not null default ''; -update share -set description_new = description -where description is not null; -alter table share - drop description; -alter table share - rename description_new to description; - -alter table share - add resource_type_new varchar not null default ''; -update share -set resource_type_new = resource_type -where resource_type is not null; -alter table share - drop resource_type; -alter table share - rename resource_type_new to resource_type; - -alter table share - add contents_new varchar not null default ''; -update share -set contents_new = contents -where contents is not null; -alter table share - drop contents; -alter table share - rename contents_new to contents; - -alter table share - add format_new varchar not null default ''; -update share -set format_new = format -where format is not null; -alter table share - drop format; -alter table share - rename format_new to format; - -alter table share - add max_bit_rate_new integer not null default 0; -update share -set max_bit_rate_new = max_bit_rate -where max_bit_rate is not null; -alter table share - drop max_bit_rate; -alter table share - rename max_bit_rate_new to max_bit_rate; - -alter table share - add visit_count_new integer not null default 0; -update share -set visit_count_new = visit_count -where visit_count is not null; -alter table share - drop visit_count; -alter table share - rename visit_count_new to visit_count; - --- INDEX -create index album_alphabetical_by_artist - on album (compilation, order_album_artist_name, order_album_name); - -create index album_order_album_name - on album (order_album_name); - -create index album_order_album_artist_name - on album (order_album_artist_name); - -create index album_mbz_album_type - on album (mbz_album_type); - -create index artist_order_artist_name - on artist (order_artist_name); - -create index media_file_order_album_name - on media_file (order_album_name); - -create index media_file_order_artist_name - on media_file (order_artist_name); - -create index media_file_order_title - on media_file (order_title); - -create index media_file_bpm - on media_file (bpm); - -create index media_file_channels - on media_file (channels); - -create index media_file_mbz_track_id - on media_file (mbz_recording_id); - -`) - return err -} - -func Down20240122223340(context.Context, *sql.Tx) error { - return nil -} diff --git a/db/migrations/20240426202913_add_id_to_scrobble_buffer.go b/db/migrations/20240426202913_add_id_to_scrobble_buffer.go deleted file mode 100644 index e3df63595..000000000 --- a/db/migrations/20240426202913_add_id_to_scrobble_buffer.go +++ /dev/null @@ -1,30 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddIdToScrobbleBuffer, downAddIdToScrobbleBuffer) -} - -func upAddIdToScrobbleBuffer(ctx context.Context, tx *sql.Tx) error { - _, err := tx.ExecContext(ctx, ` -delete from scrobble_buffer where user_id <> ''; -alter table scrobble_buffer add id varchar not null default ''; -create unique index scrobble_buffer_id_ix - on scrobble_buffer (id); -`) - return err -} - -func downAddIdToScrobbleBuffer(ctx context.Context, tx *sql.Tx) error { - _, err := tx.ExecContext(ctx, ` -drop index scrobble_buffer_id_ix; -alter table scrobble_buffer drop id; -`) - return err -} diff --git a/db/migrations/20240511210036_add_sample_rate.go b/db/migrations/20240511210036_add_sample_rate.go deleted file mode 100644 index 619cdcffd..000000000 --- a/db/migrations/20240511210036_add_sample_rate.go +++ /dev/null @@ -1,29 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddSampleRate, downAddSampleRate) -} - -func upAddSampleRate(ctx context.Context, tx *sql.Tx) error { - _, err := tx.ExecContext(ctx, ` -alter table media_file - add sample_rate integer not null default 0; - -create index if not exists media_file_sample_rate - on media_file (sample_rate); -`) - notice(tx, "A full rescan should be performed to pick up additional tags") - return err -} - -func downAddSampleRate(ctx context.Context, tx *sql.Tx) error { - _, err := tx.ExecContext(ctx, `alter table media_file drop sample_rate;`) - return err -} diff --git a/db/migrations/20240511220020_add_library_table.go b/db/migrations/20240511220020_add_library_table.go deleted file mode 100644 index 55b521ca9..000000000 --- a/db/migrations/20240511220020_add_library_table.go +++ /dev/null @@ -1,71 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - "fmt" - - "github.com/navidrome/navidrome/conf" - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddLibraryTable, downAddLibraryTable) -} - -func upAddLibraryTable(ctx context.Context, tx *sql.Tx) error { - _, err := tx.ExecContext(ctx, ` - create table library ( - id integer primary key autoincrement, - name text not null unique, - path text not null unique, - remote_path text null default '', - last_scan_at datetime not null default '0000-00-00 00:00:00', - updated_at datetime not null default current_timestamp, - created_at datetime not null default current_timestamp - );`) - if err != nil { - return err - } - - _, err = tx.ExecContext(ctx, fmt.Sprintf(` - insert into library(id, name, path) values(1, 'Music Library', '%s'); - delete from property where id like 'LastScan-%%'; -`, conf.Server.MusicFolder)) - if err != nil { - return err - } - - _, err = tx.ExecContext(ctx, ` - alter table media_file add column library_id integer not null default 1 - references library(id) on delete cascade; - alter table album add column library_id integer not null default 1 - references library(id) on delete cascade; - - create table if not exists library_artist - ( - library_id integer not null default 1 - references library(id) - on delete cascade, - artist_id varchar not null default null - references artist(id) - on delete cascade, - constraint library_artist_ux - unique (library_id, artist_id) - ); - - insert into library_artist(library_id, artist_id) select 1, id from artist; -`) - - return err -} - -func downAddLibraryTable(ctx context.Context, tx *sql.Tx) error { - _, err := tx.ExecContext(ctx, ` - alter table media_file drop column library_id; - alter table album drop column library_id; - drop table library_artist; - drop table library; -`) - return err -} diff --git a/db/migrations/20240629152843_remove_annotation_id.go b/db/migrations/20240629152843_remove_annotation_id.go deleted file mode 100644 index b450b26d4..000000000 --- a/db/migrations/20240629152843_remove_annotation_id.go +++ /dev/null @@ -1,66 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upRemoveAnnotationId, downRemoveAnnotationId) -} - -func upRemoveAnnotationId(ctx context.Context, tx *sql.Tx) error { - _, err := tx.ExecContext(ctx, ` -create table annotation_dg_tmp -( - user_id varchar(255) default '' not null, - item_id varchar(255) default '' not null, - item_type varchar(255) default '' not null, - play_count integer default 0, - play_date datetime, - rating integer default 0, - starred bool default FALSE not null, - starred_at datetime, - unique (user_id, item_id, item_type) -); - -insert into annotation_dg_tmp(user_id, item_id, item_type, play_count, play_date, rating, starred, starred_at) -select user_id, - item_id, - item_type, - play_count, - play_date, - rating, - starred, - starred_at -from annotation; - -drop table annotation; - -alter table annotation_dg_tmp - rename to annotation; - -create index annotation_play_count - on annotation (play_count); - -create index annotation_play_date - on annotation (play_date); - -create index annotation_rating - on annotation (rating); - -create index annotation_starred - on annotation (starred); - -create index annotation_starred_at - on annotation (starred_at); - -`) - return err -} - -func downRemoveAnnotationId(ctx context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20240802044339_player_use_user_id_over_username.go b/db/migrations/20240802044339_player_use_user_id_over_username.go deleted file mode 100644 index 6532f0f45..000000000 --- a/db/migrations/20240802044339_player_use_user_id_over_username.go +++ /dev/null @@ -1,62 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upPlayerUseUserIdOverUsername, downPlayerUseUserIdOverUsername) -} - -func upPlayerUseUserIdOverUsername(ctx context.Context, tx *sql.Tx) error { - _, err := tx.ExecContext(ctx, ` -CREATE TABLE player_dg_tmp -( - id varchar(255) not null - primary key, - name varchar not null, - user_agent varchar, - user_id varchar not null - references user (id) - on update cascade on delete cascade, - client varchar not null, - ip varchar, - last_seen timestamp, - max_bit_rate int default 0, - transcoding_id varchar, - report_real_path bool default FALSE not null, - scrobble_enabled bool default true -); - -INSERT INTO player_dg_tmp( - id, name, user_agent, user_id, client, ip, last_seen, max_bit_rate, - transcoding_id, report_real_path, scrobble_enabled -) -SELECT - id, name, user_agent, - IFNULL( - (select id from user where user_name = player.user_name), 'UNKNOWN_USERNAME' - ), - client, ip_address, last_seen, max_bit_rate, transcoding_id, report_real_path, scrobble_enabled -FROM player; - -DELETE FROM player_dg_tmp WHERE user_id = 'UNKNOWN_USERNAME'; -DROP TABLE player; -ALTER TABLE player_dg_tmp RENAME TO player; - -CREATE INDEX IF NOT EXISTS player_match - on player (client, user_agent, user_id); -CREATE INDEX IF NOT EXISTS player_name - on player (name); -`) - - return err -} - -func downPlayerUseUserIdOverUsername(ctx context.Context, tx *sql.Tx) error { - // This code is executed when the migration is rolled back. - return nil -} diff --git a/db/migrations/20241020003138_add_sort_tags_index.sql b/db/migrations/20241020003138_add_sort_tags_index.sql deleted file mode 100644 index 6d86971e7..000000000 --- a/db/migrations/20241020003138_add_sort_tags_index.sql +++ /dev/null @@ -1,9 +0,0 @@ --- +goose Up -create index if not exists media_file_sort_title on media_file(coalesce(nullif(sort_title,''),order_title)); -create index if not exists album_sort_name on album(coalesce(nullif(sort_album_name,''),order_album_name)); -create index if not exists artist_sort_name on artist(coalesce(nullif(sort_artist_name,''),order_artist_name)); - --- +goose Down -drop index if exists media_file_sort_title; -drop index if exists album_sort_name; -drop index if exists artist_sort_name; \ No newline at end of file diff --git a/db/migrations/20241024125533_fix_sort_tags_collation_and_index.sql b/db/migrations/20241024125533_fix_sort_tags_collation_and_index.sql deleted file mode 100644 index d2edde0ec..000000000 --- a/db/migrations/20241024125533_fix_sort_tags_collation_and_index.sql +++ /dev/null @@ -1,512 +0,0 @@ --- +goose Up ---region Artist Table -create table artist_dg_tmp -( - id varchar(255) not null - primary key, - name varchar(255) default '' not null, - album_count integer default 0 not null, - full_text varchar(255) default '', - song_count integer default 0 not null, - size integer default 0 not null, - biography varchar(255) default '' not null, - small_image_url varchar(255) default '' not null, - medium_image_url varchar(255) default '' not null, - large_image_url varchar(255) default '' not null, - similar_artists varchar(255) default '' not null, - external_url varchar(255) default '' not null, - external_info_updated_at datetime, - order_artist_name varchar collate NOCASE default '' not null, - sort_artist_name varchar collate NOCASE default '' not null, - mbz_artist_id varchar default '' not null -); - -insert into artist_dg_tmp(id, name, album_count, full_text, song_count, size, biography, small_image_url, - medium_image_url, large_image_url, similar_artists, external_url, external_info_updated_at, - order_artist_name, sort_artist_name, mbz_artist_id) -select id, - name, - album_count, - full_text, - song_count, - size, - biography, - small_image_url, - medium_image_url, - large_image_url, - similar_artists, - external_url, - external_info_updated_at, - order_artist_name, - sort_artist_name, - mbz_artist_id -from artist; - -drop table artist; - -alter table artist_dg_tmp - rename to artist; - -create index artist_full_text - on artist (full_text); - -create index artist_name - on artist (name); - -create index artist_order_artist_name - on artist (order_artist_name); - -create index artist_size - on artist (size); - -create index artist_sort_name - on artist (coalesce(nullif(sort_artist_name,''),order_artist_name) collate NOCASE); - ---endregion - ---region Album Table -create table album_dg_tmp -( - id varchar(255) not null - primary key, - name varchar(255) default '' not null, - artist_id varchar(255) default '' not null, - embed_art_path varchar(255) default '' not null, - artist varchar(255) default '' not null, - album_artist varchar(255) default '' not null, - min_year int default 0 not null, - max_year integer default 0 not null, - compilation bool default FALSE not null, - song_count integer default 0 not null, - duration real default 0 not null, - genre varchar(255) default '' not null, - created_at datetime, - updated_at datetime, - full_text varchar(255) default '', - album_artist_id varchar(255) default '', - size integer default 0 not null, - all_artist_ids varchar, - description varchar(255) default '' not null, - small_image_url varchar(255) default '' not null, - medium_image_url varchar(255) default '' not null, - large_image_url varchar(255) default '' not null, - external_url varchar(255) default '' not null, - external_info_updated_at datetime, - date varchar(255) default '' not null, - min_original_year int default 0 not null, - max_original_year int default 0 not null, - original_date varchar(255) default '' not null, - release_date varchar(255) default '' not null, - releases integer default 0 not null, - image_files varchar default '' not null, - order_album_name varchar collate NOCASE default '' not null, - order_album_artist_name varchar collate NOCASE default '' not null, - sort_album_name varchar collate NOCASE default '' not null, - sort_album_artist_name varchar collate NOCASE default '' not null, - catalog_num varchar default '' not null, - comment varchar default '' not null, - paths varchar default '' not null, - mbz_album_id varchar default '' not null, - mbz_album_artist_id varchar default '' not null, - mbz_album_type varchar default '' not null, - mbz_album_comment varchar default '' not null, - discs jsonb default '{}' not null, - library_id integer default 1 not null - references library - on delete cascade -); - -insert into album_dg_tmp(id, name, artist_id, embed_art_path, artist, album_artist, min_year, max_year, compilation, - song_count, duration, genre, created_at, updated_at, full_text, album_artist_id, size, - all_artist_ids, description, small_image_url, medium_image_url, large_image_url, external_url, - external_info_updated_at, date, min_original_year, max_original_year, original_date, - release_date, releases, image_files, order_album_name, order_album_artist_name, - sort_album_name, sort_album_artist_name, catalog_num, comment, paths, - mbz_album_id, mbz_album_artist_id, mbz_album_type, mbz_album_comment, discs, library_id) -select id, - name, - artist_id, - embed_art_path, - artist, - album_artist, - min_year, - max_year, - compilation, - song_count, - duration, - genre, - created_at, - updated_at, - full_text, - album_artist_id, - size, - all_artist_ids, - description, - small_image_url, - medium_image_url, - large_image_url, - external_url, - external_info_updated_at, - date, - min_original_year, - max_original_year, - original_date, - release_date, - releases, - image_files, - order_album_name, - order_album_artist_name, - sort_album_name, - sort_album_artist_name, - catalog_num, - comment, - paths, - mbz_album_id, - mbz_album_artist_id, - mbz_album_type, - mbz_album_comment, - discs, - library_id -from album; - -drop table album; - -alter table album_dg_tmp - rename to album; - -create index album_all_artist_ids - on album (all_artist_ids); - -create index album_alphabetical_by_artist - on album (compilation, order_album_artist_name, order_album_name); - -create index album_artist - on album (artist); - -create index album_artist_album - on album (artist); - -create index album_artist_album_id - on album (album_artist_id); - -create index album_artist_id - on album (artist_id); - -create index album_created_at - on album (created_at); - -create index album_full_text - on album (full_text); - -create index album_genre - on album (genre); - -create index album_max_year - on album (max_year); - -create index album_mbz_album_type - on album (mbz_album_type); - -create index album_min_year - on album (min_year); - -create index album_name - on album (name); - -create index album_order_album_artist_name - on album (order_album_artist_name); - -create index album_order_album_name - on album (order_album_name); - -create index album_size - on album (size); - -create index album_sort_name - on album (coalesce(nullif(sort_album_name,''),order_album_name) collate NOCASE); - -create index album_sort_album_artist_name - on album (coalesce(nullif(sort_album_artist_name,''),order_album_artist_name) collate NOCASE); - -create index album_updated_at - on album (updated_at); ---endregion - ---region Media File Table -create table media_file_dg_tmp -( - id varchar(255) not null - primary key, - path varchar(255) default '' not null, - title varchar(255) default '' not null, - album varchar(255) default '' not null, - artist varchar(255) default '' not null, - artist_id varchar(255) default '' not null, - album_artist varchar(255) default '' not null, - album_id varchar(255) default '' not null, - has_cover_art bool default FALSE not null, - track_number integer default 0 not null, - disc_number integer default 0 not null, - year integer default 0 not null, - size integer default 0 not null, - suffix varchar(255) default '' not null, - duration real default 0 not null, - bit_rate integer default 0 not null, - genre varchar(255) default '' not null, - compilation bool default FALSE not null, - created_at datetime, - updated_at datetime, - full_text varchar(255) default '', - album_artist_id varchar(255) default '', - date varchar(255) default '' not null, - original_year int default 0 not null, - original_date varchar(255) default '' not null, - release_year int default 0 not null, - release_date varchar(255) default '' not null, - order_album_name varchar collate NOCASE default '' not null, - order_album_artist_name varchar collate NOCASE default '' not null, - order_artist_name varchar collate NOCASE default '' not null, - sort_album_name varchar collate NOCASE default '' not null, - sort_artist_name varchar collate NOCASE default '' not null, - sort_album_artist_name varchar collate NOCASE default '' not null, - sort_title varchar collate NOCASE default '' not null, - disc_subtitle varchar default '' not null, - catalog_num varchar default '' not null, - comment varchar default '' not null, - order_title varchar collate NOCASE default '' not null, - mbz_recording_id varchar default '' not null, - mbz_album_id varchar default '' not null, - mbz_artist_id varchar default '' not null, - mbz_album_artist_id varchar default '' not null, - mbz_album_type varchar default '' not null, - mbz_album_comment varchar default '' not null, - mbz_release_track_id varchar default '' not null, - bpm integer default 0 not null, - channels integer default 0 not null, - rg_album_gain real default 0 not null, - rg_album_peak real default 0 not null, - rg_track_gain real default 0 not null, - rg_track_peak real default 0 not null, - lyrics jsonb default '[]' not null, - sample_rate integer default 0 not null, - library_id integer default 1 not null - references library - on delete cascade -); - -insert into media_file_dg_tmp(id, path, title, album, artist, artist_id, album_artist, album_id, has_cover_art, - track_number, disc_number, year, size, suffix, duration, bit_rate, genre, compilation, - created_at, updated_at, full_text, album_artist_id, date, original_year, original_date, - release_year, release_date, order_album_name, order_album_artist_name, order_artist_name, - sort_album_name, sort_artist_name, sort_album_artist_name, sort_title, disc_subtitle, - catalog_num, comment, order_title, mbz_recording_id, mbz_album_id, mbz_artist_id, - mbz_album_artist_id, mbz_album_type, mbz_album_comment, mbz_release_track_id, bpm, - channels, rg_album_gain, rg_album_peak, rg_track_gain, rg_track_peak, lyrics, sample_rate, - library_id) -select id, - path, - title, - album, - artist, - artist_id, - album_artist, - album_id, - has_cover_art, - track_number, - disc_number, - year, - size, - suffix, - duration, - bit_rate, - genre, - compilation, - created_at, - updated_at, - full_text, - album_artist_id, - date, - original_year, - original_date, - release_year, - release_date, - order_album_name, - order_album_artist_name, - order_artist_name, - sort_album_name, - sort_artist_name, - sort_album_artist_name, - sort_title, - disc_subtitle, - catalog_num, - comment, - order_title, - mbz_recording_id, - mbz_album_id, - mbz_artist_id, - mbz_album_artist_id, - mbz_album_type, - mbz_album_comment, - mbz_release_track_id, - bpm, - channels, - rg_album_gain, - rg_album_peak, - rg_track_gain, - rg_track_peak, - lyrics, - sample_rate, - library_id -from media_file; - -drop table media_file; - -alter table media_file_dg_tmp - rename to media_file; - -create index media_file_album_artist - on media_file (album_artist); - -create index media_file_album_id - on media_file (album_id); - -create index media_file_artist - on media_file (artist); - -create index media_file_artist_album_id - on media_file (album_artist_id); - -create index media_file_artist_id - on media_file (artist_id); - -create index media_file_bpm - on media_file (bpm); - -create index media_file_channels - on media_file (channels); - -create index media_file_created_at - on media_file (created_at); - -create index media_file_duration - on media_file (duration); - -create index media_file_full_text - on media_file (full_text); - -create index media_file_genre - on media_file (genre); - -create index media_file_mbz_track_id - on media_file (mbz_recording_id); - -create index media_file_order_album_name - on media_file (order_album_name); - -create index media_file_order_artist_name - on media_file (order_artist_name); - -create index media_file_order_title - on media_file (order_title); - -create index media_file_path - on media_file (path); - -create index media_file_path_nocase - on media_file (path collate NOCASE); - -create index media_file_sample_rate - on media_file (sample_rate); - -create index media_file_sort_title - on media_file (coalesce(nullif(sort_title,''),order_title) collate NOCASE); - -create index media_file_sort_artist_name - on media_file (coalesce(nullif(sort_artist_name,''),order_artist_name) collate NOCASE); - -create index media_file_sort_album_name - on media_file (coalesce(nullif(sort_album_name,''),order_album_name) collate NOCASE); - -create index media_file_title - on media_file (title); - -create index media_file_track_number - on media_file (disc_number, track_number); - -create index media_file_updated_at - on media_file (updated_at); - -create index media_file_year - on media_file (year); - ---endregion - ---region Radio Table -create table radio_dg_tmp -( - id varchar(255) not null - primary key, - name varchar collate NOCASE not null - unique, - stream_url varchar not null, - home_page_url varchar default '' not null, - created_at datetime, - updated_at datetime -); - -insert into radio_dg_tmp(id, name, stream_url, home_page_url, created_at, updated_at) -select id, name, stream_url, home_page_url, created_at, updated_at -from radio; - -drop table radio; - -alter table radio_dg_tmp - rename to radio; - -create index radio_name - on radio(name); ---endregion - ---region users Table -create table user_dg_tmp -( - id varchar(255) not null - primary key, - user_name varchar(255) default '' not null - unique, - name varchar(255) collate NOCASE default '' not null, - email varchar(255) default '' not null, - password varchar(255) default '' not null, - is_admin bool default FALSE not null, - last_login_at datetime, - last_access_at datetime, - created_at datetime not null, - updated_at datetime not null -); - -insert into user_dg_tmp(id, user_name, name, email, password, is_admin, last_login_at, last_access_at, created_at, - updated_at) -select id, - user_name, - name, - email, - password, - is_admin, - last_login_at, - last_access_at, - created_at, - updated_at -from user; - -drop table user; - -alter table user_dg_tmp - rename to user; - -create index user_username_password - on user(user_name collate NOCASE, password); ---endregion - --- +goose Down -alter table album - add column sort_artist_name varchar default '' not null; diff --git a/db/migrations/20241026183640_support_new_scanner.go b/db/migrations/20241026183640_support_new_scanner.go deleted file mode 100644 index fcbef7e4e..000000000 --- a/db/migrations/20241026183640_support_new_scanner.go +++ /dev/null @@ -1,319 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - "fmt" - "io/fs" - "os" - "path/filepath" - "strings" - "testing/fstest" - "unicode/utf8" - - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/model" - "github.com/navidrome/navidrome/utils/run" - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upSupportNewScanner, downSupportNewScanner) -} - -func upSupportNewScanner(ctx context.Context, tx *sql.Tx) error { - execute := createExecuteFunc(ctx, tx) - addColumn := createAddColumnFunc(ctx, tx) - - return run.Sequentially( - upSupportNewScanner_CreateTableFolder(ctx, execute), - upSupportNewScanner_PopulateTableFolder(ctx, tx), - upSupportNewScanner_UpdateTableMediaFile(ctx, execute, addColumn), - upSupportNewScanner_UpdateTableAlbum(ctx, execute), - upSupportNewScanner_UpdateTableArtist(ctx, execute, addColumn), - execute(` -alter table library - add column last_scan_started_at datetime default '0000-00-00 00:00:00' not null; -alter table library - add column full_scan_in_progress boolean default false not null; - -create table if not exists media_file_artists( - media_file_id varchar not null - references media_file (id) - on delete cascade, - artist_id varchar not null - references artist (id) - on delete cascade, - role varchar default '' not null, - sub_role varchar default '' not null, - constraint artist_tracks - unique (artist_id, media_file_id, role, sub_role) -); -create index if not exists media_file_artists_media_file_id - on media_file_artists (media_file_id); -create index if not exists media_file_artists_role - on media_file_artists (role); - -create table if not exists album_artists( - album_id varchar not null - references album (id) - on delete cascade, - artist_id varchar not null - references artist (id) - on delete cascade, - role varchar default '' not null, - sub_role varchar default '' not null, - constraint album_artists - unique (album_id, artist_id, role, sub_role) -); -create index if not exists album_artists_album_id - on album_artists (album_id); -create index if not exists album_artists_role - on album_artists (role); - -create table if not exists tag( - id varchar not null primary key, - tag_name varchar default '' not null, - tag_value varchar default '' not null, - album_count integer default 0 not null, - media_file_count integer default 0 not null, - constraint tags_name_value - unique (tag_name, tag_value) -); - --- Genres are now stored in the tag table -drop table if exists media_file_genres; -drop table if exists album_genres; -drop table if exists artist_genres; -drop table if exists genre; - --- Drop full_text indexes, as they are not being used by SQLite -drop index if exists media_file_full_text; -drop index if exists album_full_text; -drop index if exists artist_full_text; - --- Add PID config to properties -insert into property (id, value) values ('PIDTrack', 'track_legacy') on conflict do nothing; -insert into property (id, value) values ('PIDAlbum', 'album_legacy') on conflict do nothing; -`), - func() error { - notice(tx, "A full scan will be triggered to populate the new tables. This may take a while.") - return forceFullRescan(tx) - }, - ) -} - -func upSupportNewScanner_CreateTableFolder(_ context.Context, execute execStmtFunc) execFunc { - return execute(` -create table if not exists folder( - id varchar not null - primary key, - library_id integer not null - references library (id) - on delete cascade, - path varchar default '' not null, - name varchar default '' not null, - missing boolean default false not null, - parent_id varchar default '' not null, - num_audio_files integer default 0 not null, - num_playlists integer default 0 not null, - image_files jsonb default '[]' not null, - images_updated_at datetime default '0000-00-00 00:00:00' not null, - updated_at datetime default (datetime(current_timestamp, 'localtime')) not null, - created_at datetime default (datetime(current_timestamp, 'localtime')) not null -); -create index folder_parent_id on folder(parent_id); -`) -} - -// Use paths from `media_file` table to populate `folder` table. The `folder` table must contain all paths, including -// the ones that do not contain any media_file. We can get all paths from the media_file table to populate a -// fstest.MapFS{}, and then walk the filesystem to insert all folders into the DB, including empty parent ones. -func upSupportNewScanner_PopulateTableFolder(ctx context.Context, tx *sql.Tx) execFunc { - return func() error { - // First, get all folder paths from media_file table - rows, err := tx.QueryContext(ctx, fmt.Sprintf(` -select distinct rtrim(media_file.path, replace(media_file.path, '%s', '')), library_id, library.path -from media_file -join library on media_file.library_id = library.id`, string(os.PathSeparator))) - if err != nil { - return err - } - defer rows.Close() - - // Then create an in-memory filesystem with all paths - var path string - var lib model.Library - fsys := fstest.MapFS{} - - for rows.Next() { - err = rows.Scan(&path, &lib.ID, &lib.Path) - if err != nil { - return err - } - - path = strings.TrimPrefix(path, filepath.Clean(lib.Path)) - path = strings.TrimPrefix(path, string(os.PathSeparator)) - path = filepath.Clean(path) - fsys[path] = &fstest.MapFile{Mode: fs.ModeDir} - } - if err = rows.Err(); err != nil { - return fmt.Errorf("error loading folders from media_file table: %w", err) - } - if len(fsys) == 0 { - return nil - } - - stmt, err := tx.PrepareContext(ctx, - "insert into folder (id, library_id, path, name, parent_id, updated_at) values (?, ?, ?, ?, ?, '0000-00-00 00:00:00')", - ) - if err != nil { - return err - } - - // Finally, walk the in-mem filesystem and insert all folders into the DB. - err = fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - // Don't abort the walk, just log the error - log.Error("error walking folder to DB", "path", path, err) - return nil - } - // Skip entries that are not directories - if !d.IsDir() { - return nil - } - - // Create a folder in the DB - f := model.NewFolder(lib, path) - _, err = stmt.ExecContext(ctx, f.ID, lib.ID, f.Path, f.Name, f.ParentID) - if err != nil { - log.Error("error writing folder to DB", "path", path, err) - } - return err - }) - if err != nil { - return fmt.Errorf("error populating folder table: %w", err) - } - - // Count the number of characters in the library path - libPath := filepath.Clean(lib.Path) - libPathLen := utf8.RuneCountInString(libPath) - - // In one go, update all paths in the media_file table, removing the library path prefix - // and replacing any backslashes with slashes (the path separator used by the io/fs package) - _, err = tx.ExecContext(ctx, fmt.Sprintf(` -update media_file set path = replace(substr(path, %d), '\', '/');`, libPathLen+2)) - if err != nil { - return fmt.Errorf("error updating media_file path: %w", err) - } - - return nil - } -} - -func upSupportNewScanner_UpdateTableMediaFile(_ context.Context, execute execStmtFunc, addColumn addColumnFunc) execFunc { - return func() error { - return run.Sequentially( - execute(` -alter table media_file - add column folder_id varchar default '' not null; -alter table media_file - add column pid varchar default '' not null; -alter table media_file - add column missing boolean default false not null; -alter table media_file - add column mbz_release_group_id varchar default '' not null; -alter table media_file - add column tags jsonb default '{}' not null; -alter table media_file - add column participants jsonb default '{}' not null; -alter table media_file - add column bit_depth integer default 0 not null; -alter table media_file - add column explicit_status varchar default '' not null; -`), - addColumn("media_file", "birth_time", "datetime", "current_timestamp", "created_at"), - execute(` -update media_file - set pid = id where pid = ''; -create index if not exists media_file_birth_time - on media_file (birth_time); -create index if not exists media_file_folder_id - on media_file (folder_id); -create index if not exists media_file_pid - on media_file (pid); -create index if not exists media_file_missing - on media_file (missing); -`), - ) - } -} - -func upSupportNewScanner_UpdateTableAlbum(_ context.Context, execute execStmtFunc) execFunc { - return execute(` -drop index if exists album_all_artist_ids; -alter table album - drop column all_artist_ids; -drop index if exists album_artist; -drop index if exists album_artist_album; -alter table album - drop column artist; -drop index if exists album_artist_id; -alter table album - drop column artist_id; -alter table album - add column imported_at datetime default '0000-00-00 00:00:00' not null; -alter table album - add column missing boolean default false not null; -alter table album - add column mbz_release_group_id varchar default '' not null; -alter table album - add column tags jsonb default '{}' not null; -alter table album - add column participants jsonb default '{}' not null; -alter table album - drop column paths; -alter table album - drop column image_files; -alter table album - add column folder_ids jsonb default '[]' not null; -alter table album - add column explicit_status varchar default '' not null; -create index if not exists album_imported_at - on album (imported_at); -create index if not exists album_mbz_release_group_id - on album (mbz_release_group_id); -`) -} - -func upSupportNewScanner_UpdateTableArtist(_ context.Context, execute execStmtFunc, addColumn addColumnFunc) execFunc { - return func() error { - return run.Sequentially( - execute(` -alter table artist - drop column album_count; -alter table artist - drop column song_count; -drop index if exists artist_size; -alter table artist - drop column size; -alter table artist - add column missing boolean default false not null; -alter table artist - add column stats jsonb default '{"albumartist":{}}' not null; -alter table artist - drop column similar_artists; -alter table artist - add column similar_artists jsonb default '[]' not null; -`), - addColumn("artist", "updated_at", "datetime", "current_time", "(select min(album.updated_at) from album where album_artist_id = artist.id)"), - addColumn("artist", "created_at", "datetime", "current_time", "(select min(album.created_at) from album where album_artist_id = artist.id)"), - execute(`create index if not exists artist_updated_at on artist (updated_at);`), - execute(`update artist set external_info_updated_at = '0000-00-00 00:00:00';`), - ) - } -} - -func downSupportNewScanner(context.Context, *sql.Tx) error { - return nil -} diff --git a/db/migrations/20250522013904_share_user_id_on_delete.sql b/db/migrations/20250522013904_share_user_id_on_delete.sql deleted file mode 100644 index 91ff653ac..000000000 --- a/db/migrations/20250522013904_share_user_id_on_delete.sql +++ /dev/null @@ -1,36 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE share_tmp -( - id varchar(255) not null - primary key, - expires_at datetime, - last_visited_at datetime, - resource_ids varchar not null, - created_at datetime, - updated_at datetime, - user_id varchar(255) not null - constraint share_user_id_fk - references user - on update cascade on delete cascade, - downloadable bool not null default false, - description varchar not null default '', - resource_type varchar not null default '', - contents varchar not null default '', - format varchar not null default '', - max_bit_rate integer not null default 0, - visit_count integer not null default 0 -); - - -INSERT INTO share_tmp( - id, expires_at, last_visited_at, resource_ids, created_at, updated_at, user_id, downloadable, description, resource_type, contents, format, max_bit_rate, visit_count -) SELECT id, expires_at, last_visited_at, resource_ids, created_at, updated_at, user_id, downloadable, description, resource_type, contents, format, max_bit_rate, visit_count -FROM share; - -DROP TABLE share; - -ALTER TABLE share_tmp RENAME To share; --- +goose StatementEnd - --- +goose Down diff --git a/db/migrations/20250611010101_playqueue_current_to_index.go b/db/migrations/20250611010101_playqueue_current_to_index.go deleted file mode 100644 index d9250eba2..000000000 --- a/db/migrations/20250611010101_playqueue_current_to_index.go +++ /dev/null @@ -1,80 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - "strings" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upPlayQueueCurrentToIndex, downPlayQueueCurrentToIndex) -} - -func upPlayQueueCurrentToIndex(ctx context.Context, tx *sql.Tx) error { - _, err := tx.ExecContext(ctx, ` -create table playqueue_dg_tmp( - id varchar(255) not null, - user_id varchar(255) not null - references user(id) - on update cascade on delete cascade, - current integer not null default 0, - position real, - changed_by varchar(255), - items varchar(255), - created_at datetime, - updated_at datetime -);`) - if err != nil { - return err - } - - rows, err := tx.QueryContext(ctx, `select id, user_id, current, position, changed_by, items, created_at, updated_at from playqueue`) - if err != nil { - return err - } - defer rows.Close() - - stmt, err := tx.PrepareContext(ctx, `insert into playqueue_dg_tmp(id, user_id, current, position, changed_by, items, created_at, updated_at) values(?,?,?,?,?,?,?,?)`) - if err != nil { - return err - } - defer stmt.Close() - - for rows.Next() { - var id, userID, currentID, changedBy, items string - var position sql.NullFloat64 - var createdAt, updatedAt sql.NullString - if err = rows.Scan(&id, &userID, ¤tID, &position, &changedBy, &items, &createdAt, &updatedAt); err != nil { - return err - } - index := 0 - if currentID != "" && items != "" { - parts := strings.Split(items, ",") - for i, p := range parts { - if p == currentID { - index = i - break - } - } - } - _, err = stmt.Exec(id, userID, index, position, changedBy, items, createdAt, updatedAt) - if err != nil { - return err - } - } - if err = rows.Err(); err != nil { - return err - } - - if _, err = tx.ExecContext(ctx, `drop table playqueue;`); err != nil { - return err - } - _, err = tx.ExecContext(ctx, `alter table playqueue_dg_tmp rename to playqueue;`) - return err -} - -func downPlayQueueCurrentToIndex(ctx context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20250701010101_add_folder_hash.go b/db/migrations/20250701010101_add_folder_hash.go deleted file mode 100644 index e82a0749f..000000000 --- a/db/migrations/20250701010101_add_folder_hash.go +++ /dev/null @@ -1,21 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddFolderHash, downAddFolderHash) -} - -func upAddFolderHash(ctx context.Context, tx *sql.Tx) error { - _, err := tx.ExecContext(ctx, `alter table folder add column hash varchar default '' not null;`) - return err -} - -func downAddFolderHash(ctx context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20250701010102_add_annotation_user_foreign_key.sql b/db/migrations/20250701010102_add_annotation_user_foreign_key.sql deleted file mode 100644 index 114de2a88..000000000 --- a/db/migrations/20250701010102_add_annotation_user_foreign_key.sql +++ /dev/null @@ -1,46 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS annotation_tmp -( - user_id varchar(255) not null - REFERENCES user(id) - ON DELETE CASCADE - ON UPDATE CASCADE, - item_id varchar(255) default '' not null, - item_type varchar(255) default '' not null, - play_count integer default 0, - play_date datetime, - rating integer default 0, - starred bool default FALSE not null, - starred_at datetime, - unique (user_id, item_id, item_type) -); - - -INSERT INTO annotation_tmp( - user_id, item_id, item_type, play_count, play_date, rating, starred, starred_at -) -SELECT user_id, item_id, item_type, play_count, play_date, rating, starred, starred_at -FROM annotation -WHERE user_id IN ( - SELECT id FROM user -); - -DROP TABLE annotation; -ALTER TABLE annotation_tmp RENAME TO annotation; - -CREATE INDEX annotation_play_count - on annotation (play_count); -CREATE INDEX annotation_play_date - on annotation (play_date); -CREATE INDEX annotation_rating - on annotation (rating); -CREATE INDEX annotation_starred - on annotation (starred); -CREATE INDEX annotation_starred_at - on annotation (starred_at); - --- +goose StatementEnd - --- +goose Down - diff --git a/db/migrations/20250701010103_add_library_stats.go b/db/migrations/20250701010103_add_library_stats.go deleted file mode 100644 index 8025229cc..000000000 --- a/db/migrations/20250701010103_add_library_stats.go +++ /dev/null @@ -1,48 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddLibraryStats, downAddLibraryStats) -} - -func upAddLibraryStats(ctx context.Context, tx *sql.Tx) error { - _, err := tx.ExecContext(ctx, ` -alter table library add column total_songs integer default 0 not null; -alter table library add column total_albums integer default 0 not null; -alter table library add column total_artists integer default 0 not null; -alter table library add column total_folders integer default 0 not null; - alter table library add column total_files integer default 0 not null; - alter table library add column total_missing_files integer default 0 not null; - alter table library add column total_size integer default 0 not null; -update library set - total_songs = ( - select count(*) from media_file where library_id = library.id and missing = 0 - ), - total_albums = (select count(*) from album where library_id = library.id and missing = 0), - total_artists = ( - select count(*) from library_artist la - join artist a on la.artist_id = a.id - where la.library_id = library.id and a.missing = 0 - ), - total_folders = (select count(*) from folder where library_id = library.id and missing = 0 and num_audio_files > 0), - total_files = ( - select ifnull(sum(num_audio_files + num_playlists + json_array_length(image_files)),0) - from folder where library_id = library.id and missing = 0 - ), - total_missing_files = ( - select count(*) from media_file where library_id = library.id and missing = 1 - ), - total_size = (select ifnull(sum(size),0) from album where library_id = library.id and missing = 0); -`) - return err -} - -func downAddLibraryStats(ctx context.Context, tx *sql.Tx) error { - return nil -} diff --git a/db/migrations/20250701010104_make_replaygain_fields_nullable.go b/db/migrations/20250701010104_make_replaygain_fields_nullable.go deleted file mode 100644 index 163608d32..000000000 --- a/db/migrations/20250701010104_make_replaygain_fields_nullable.go +++ /dev/null @@ -1,49 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upMakeReplaygainFieldsNullable, downMakeReplaygainFieldsNullable) -} - -func upMakeReplaygainFieldsNullable(ctx context.Context, tx *sql.Tx) error { - _, err := tx.ExecContext(ctx, ` -ALTER TABLE media_file ADD COLUMN rg_album_gain_new real; -ALTER TABLE media_file ADD COLUMN rg_album_peak_new real; -ALTER TABLE media_file ADD COLUMN rg_track_gain_new real; -ALTER TABLE media_file ADD COLUMN rg_track_peak_new real; - -UPDATE media_file SET - rg_album_gain_new = rg_album_gain, - rg_album_peak_new = rg_album_peak, - rg_track_gain_new = rg_track_gain, - rg_track_peak_new = rg_track_peak; - -ALTER TABLE media_file DROP COLUMN rg_album_gain; -ALTER TABLE media_file DROP COLUMN rg_album_peak; -ALTER TABLE media_file DROP COLUMN rg_track_gain; -ALTER TABLE media_file DROP COLUMN rg_track_peak; - -ALTER TABLE media_file RENAME COLUMN rg_album_gain_new TO rg_album_gain; -ALTER TABLE media_file RENAME COLUMN rg_album_peak_new TO rg_album_peak; -ALTER TABLE media_file RENAME COLUMN rg_track_gain_new TO rg_track_gain; -ALTER TABLE media_file RENAME COLUMN rg_track_peak_new TO rg_track_peak; - `) - - if err != nil { - return err - } - - notice(tx, "Fetching replaygain fields properly will require a full scan") - return nil -} - -func downMakeReplaygainFieldsNullable(ctx context.Context, tx *sql.Tx) error { - // This code is executed when the migration is rolled back. - return nil -} diff --git a/db/migrations/20250701010105_remove_dangling_items.sql b/db/migrations/20250701010105_remove_dangling_items.sql deleted file mode 100644 index aede49b6e..000000000 --- a/db/migrations/20250701010105_remove_dangling_items.sql +++ /dev/null @@ -1,7 +0,0 @@ --- +goose Up --- +goose StatementBegin -update media_file set missing = 1 where folder_id = ''; -update album set missing = 1 where folder_ids = '[]'; --- +goose StatementEnd - --- +goose Down diff --git a/db/migrations/20250701010106_add_participant_stats_to_all_artists.sql b/db/migrations/20250701010106_add_participant_stats_to_all_artists.sql deleted file mode 100644 index 1cd67dc32..000000000 --- a/db/migrations/20250701010106_add_participant_stats_to_all_artists.sql +++ /dev/null @@ -1,65 +0,0 @@ --- +goose Up --- +goose StatementBegin -WITH artist_role_counters AS ( - SELECT jt.atom AS artist_id, - substr( - replace(jt.path, '$.', ''), - 1, - CASE WHEN instr(replace(jt.path, '$.', ''), '[') > 0 - THEN instr(replace(jt.path, '$.', ''), '[') - 1 - ELSE length(replace(jt.path, '$.', '')) - END - ) AS role, - count(DISTINCT mf.album_id) AS album_count, - count(mf.id) AS count, - sum(mf.size) AS size - FROM media_file mf - JOIN json_tree(mf.participants) jt ON jt.key = 'id' AND jt.atom IS NOT NULL - GROUP BY jt.atom, role -), -artist_total_counters AS ( - SELECT mfa.artist_id, - 'total' AS role, - count(DISTINCT mf.album_id) AS album_count, - count(DISTINCT mf.id) AS count, - sum(mf.size) AS size - FROM media_file_artists mfa - JOIN media_file mf ON mfa.media_file_id = mf.id - GROUP BY mfa.artist_id -), -artist_participant_counter AS ( - SELECT mfa.artist_id, - 'maincredit' AS role, - count(DISTINCT mf.album_id) AS album_count, - count(DISTINCT mf.id) AS count, - sum(mf.size) AS size - FROM media_file_artists mfa - JOIN media_file mf ON mfa.media_file_id = mf.id - AND mfa.role IN ('albumartist', 'artist') - GROUP BY mfa.artist_id -), -combined_counters AS ( - SELECT artist_id, role, album_count, count, size FROM artist_role_counters - UNION - SELECT artist_id, role, album_count, count, size FROM artist_total_counters - UNION - SELECT artist_id, role, album_count, count, size FROM artist_participant_counter -), -artist_counters AS ( - SELECT artist_id AS id, - json_group_object( - replace(role, '"', ''), - json_object('a', album_count, 'm', count, 's', size) - ) AS counters - FROM combined_counters - GROUP BY artist_id -) -UPDATE artist -SET stats = coalesce((SELECT counters FROM artist_counters ac WHERE ac.id = artist.id), '{}'), - updated_at = datetime(current_timestamp, 'localtime') -WHERE artist.id <> ''; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin --- +goose StatementEnd diff --git a/db/migrations/20250701010107_add_mbid_indexes.sql b/db/migrations/20250701010107_add_mbid_indexes.sql deleted file mode 100644 index f8a5a444b..000000000 --- a/db/migrations/20250701010107_add_mbid_indexes.sql +++ /dev/null @@ -1,27 +0,0 @@ --- +goose Up --- +goose StatementBegin - --- Add indexes for MBID fields to improve lookup performance --- Artists table -create index if not exists artist_mbz_artist_id - on artist (mbz_artist_id); - --- Albums table -create index if not exists album_mbz_album_id - on album (mbz_album_id); - --- Media files table -create index if not exists media_file_mbz_release_track_id - on media_file (mbz_release_track_id); - --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin - --- Remove MBID indexes -drop index if exists artist_mbz_artist_id; -drop index if exists album_mbz_album_id; -drop index if exists media_file_mbz_release_track_id; - --- +goose StatementEnd diff --git a/db/migrations/20250701010108_add_multi_library_support.go b/db/migrations/20250701010108_add_multi_library_support.go deleted file mode 100644 index 654784d09..000000000 --- a/db/migrations/20250701010108_add_multi_library_support.go +++ /dev/null @@ -1,119 +0,0 @@ -package migrations - -import ( - "context" - "database/sql" - - "github.com/pressly/goose/v3" -) - -func init() { - goose.AddMigrationContext(upAddMultiLibrarySupport, downAddMultiLibrarySupport) -} - -func upAddMultiLibrarySupport(ctx context.Context, tx *sql.Tx) error { - _, err := tx.ExecContext(ctx, ` - -- Create user_library association table - CREATE TABLE user_library ( - user_id VARCHAR(255) NOT NULL, - library_id INTEGER NOT NULL, - PRIMARY KEY (user_id, library_id), - FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE, - FOREIGN KEY (library_id) REFERENCES library(id) ON DELETE CASCADE - ); - -- Create indexes for performance - CREATE INDEX idx_user_library_user_id ON user_library(user_id); - CREATE INDEX idx_user_library_library_id ON user_library(library_id); - - -- Populate with existing users having access to library ID 1 (existing setup) - -- Admin users get access to all libraries, regular users get access to library 1 - INSERT INTO user_library (user_id, library_id) - SELECT u.id, 1 - FROM user u; - - -- Add total_duration column to library table - ALTER TABLE library ADD COLUMN total_duration real DEFAULT 0; - UPDATE library SET total_duration = ( - SELECT IFNULL(SUM(duration),0) from album where album.library_id = library.id and missing = 0 - ); - - -- Add default_new_users column to library table - ALTER TABLE library ADD COLUMN default_new_users boolean DEFAULT false; - -- Set library ID 1 (default library) as default for new users - UPDATE library SET default_new_users = true WHERE id = 1; - - -- Add stats column to library_artist junction table for per-library artist statistics - ALTER TABLE library_artist ADD COLUMN stats text DEFAULT '{}'; - - -- Migrate existing global artist stats to per-library format in library_artist table - -- For each library_artist association, copy the artist's global stats - UPDATE library_artist - SET stats = ( - SELECT COALESCE(artist.stats, '{}') - FROM artist - WHERE artist.id = library_artist.artist_id - ); - - -- Remove stats column from artist table to eliminate duplication - -- Stats are now stored per-library in library_artist table - ALTER TABLE artist DROP COLUMN stats; - - -- Create library_tag table for per-library tag statistics - CREATE TABLE library_tag ( - tag_id VARCHAR NOT NULL, - library_id INTEGER NOT NULL, - album_count INTEGER DEFAULT 0 NOT NULL, - media_file_count INTEGER DEFAULT 0 NOT NULL, - PRIMARY KEY (tag_id, library_id), - FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE, - FOREIGN KEY (library_id) REFERENCES library(id) ON DELETE CASCADE - ); - - -- Create indexes for optimal query performance - CREATE INDEX idx_library_tag_tag_id ON library_tag(tag_id); - CREATE INDEX idx_library_tag_library_id ON library_tag(library_id); - - -- Migrate existing tag stats to per-library format in library_tag table - -- For existing installations, copy current global stats to library ID 1 (default library) - INSERT INTO library_tag (tag_id, library_id, album_count, media_file_count) - SELECT t.id, 1, t.album_count, t.media_file_count - FROM tag t - WHERE EXISTS (SELECT 1 FROM library WHERE id = 1); - - -- Remove global stats from tag table as they are now per-library - ALTER TABLE tag DROP COLUMN album_count; - ALTER TABLE tag DROP COLUMN media_file_count; - `) - - return err -} - -func downAddMultiLibrarySupport(ctx context.Context, tx *sql.Tx) error { - _, err := tx.ExecContext(ctx, ` - -- Restore stats column to artist table before removing from library_artist - ALTER TABLE artist ADD COLUMN stats text DEFAULT '{}'; - - -- Restore global stats by aggregating from library_artist (simplified approach) - -- In a real rollback scenario, this might need more sophisticated logic - UPDATE artist - SET stats = ( - SELECT COALESCE(la.stats, '{}') - FROM library_artist la - WHERE la.artist_id = artist.id - LIMIT 1 - ); - - ALTER TABLE library_artist DROP COLUMN IF EXISTS stats; - DROP INDEX IF EXISTS idx_user_library_library_id; - DROP INDEX IF EXISTS idx_user_library_user_id; - DROP TABLE IF EXISTS user_library; - ALTER TABLE library DROP COLUMN IF EXISTS total_duration; - ALTER TABLE library DROP COLUMN IF EXISTS default_new_users; - - -- Drop library_tag table and its indexes - DROP INDEX IF EXISTS idx_library_tag_library_id; - DROP INDEX IF EXISTS idx_library_tag_tag_id; - DROP TABLE IF EXISTS library_tag; - `) - return err -} diff --git a/db/migrations/20251027000000_create_schema.sql b/db/migrations/20251027000000_create_schema.sql new file mode 100644 index 000000000..a24f85725 --- /dev/null +++ b/db/migrations/20251027000000_create_schema.sql @@ -0,0 +1,489 @@ +-- +goose Up +-- PostgreSQL Schema for Navidrome +-- Converted from SQLite schema + +-- Enable extensions +CREATE EXTENSION IF NOT EXISTS citext; + +-- Property table +CREATE TABLE property ( + id VARCHAR NOT NULL PRIMARY KEY, + value VARCHAR DEFAULT '' NOT NULL +); + +-- Transcoding table +CREATE TABLE transcoding ( + id VARCHAR NOT NULL PRIMARY KEY, + name VARCHAR NOT NULL UNIQUE, + target_format VARCHAR NOT NULL UNIQUE, + command VARCHAR DEFAULT '' NOT NULL, + default_bit_rate SMALLINT DEFAULT 192 +); + +-- User table +CREATE TABLE "user" ( + id VARCHAR NOT NULL PRIMARY KEY, + user_name CITEXT NOT NULL UNIQUE, -- CITEXT for case-insensitive username + name VARCHAR DEFAULT '' NOT NULL, + email VARCHAR DEFAULT '' NOT NULL, + password VARCHAR DEFAULT '' NOT NULL, + is_admin BOOLEAN DEFAULT FALSE NOT NULL, + last_login_at TIMESTAMP, + last_access_at TIMESTAMP, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL +); + +CREATE INDEX user_username_password ON "user"(user_name, password); + +-- Library table +CREATE TABLE library ( + id SERIAL PRIMARY KEY, + name VARCHAR NOT NULL UNIQUE, + path VARCHAR NOT NULL UNIQUE, + remote_path VARCHAR DEFAULT '', + last_scan_at TIMESTAMP NOT NULL DEFAULT '1970-01-01 00:00:00', + last_scan_started_at TIMESTAMP DEFAULT '1970-01-01 00:00:00' NOT NULL, + full_scan_in_progress BOOLEAN DEFAULT FALSE NOT NULL, + total_songs BIGINT DEFAULT 0 NOT NULL, + total_albums BIGINT DEFAULT 0 NOT NULL, + total_artists BIGINT DEFAULT 0 NOT NULL, + total_folders BIGINT DEFAULT 0 NOT NULL, + total_files BIGINT DEFAULT 0 NOT NULL, + total_missing_files BIGINT DEFAULT 0 NOT NULL, + total_size BIGINT DEFAULT 0 NOT NULL, + total_duration REAL DEFAULT 0, + default_new_users BOOLEAN DEFAULT FALSE, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Artist table +CREATE TABLE artist ( + id VARCHAR NOT NULL PRIMARY KEY, + name VARCHAR DEFAULT '' NOT NULL, + full_text VARCHAR DEFAULT '', + biography VARCHAR DEFAULT '' NOT NULL, -- VARCHAR: biographies can be long + small_image_url VARCHAR DEFAULT '' NOT NULL, + medium_image_url VARCHAR DEFAULT '' NOT NULL, + large_image_url VARCHAR DEFAULT '' NOT NULL, + external_url VARCHAR DEFAULT '' NOT NULL, + external_info_updated_at TIMESTAMP, + order_artist_name VARCHAR DEFAULT '' NOT NULL, + sort_artist_name VARCHAR DEFAULT '' NOT NULL, + mbz_artist_id VARCHAR DEFAULT '' NOT NULL, + missing BOOLEAN DEFAULT FALSE NOT NULL, + similar_artists JSONB DEFAULT '[]' NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL +); + +CREATE INDEX artist_name ON artist(name); +CREATE INDEX artist_order_artist_name ON artist(order_artist_name); +CREATE INDEX artist_sort_name ON artist(COALESCE(NULLIF(sort_artist_name, ''), order_artist_name)); +CREATE INDEX artist_updated_at ON artist(updated_at); +CREATE INDEX artist_mbz_artist_id ON artist(mbz_artist_id); + +-- Library-Artist association table +CREATE TABLE library_artist ( + library_id INT NOT NULL DEFAULT 1 REFERENCES library(id) ON DELETE CASCADE, + artist_id VARCHAR NOT NULL REFERENCES artist(id) ON DELETE CASCADE, + stats VARCHAR DEFAULT '{}', + CONSTRAINT library_artist_ux UNIQUE (library_id, artist_id) +); + +-- Album table +CREATE TABLE album ( + id VARCHAR NOT NULL PRIMARY KEY, + name VARCHAR DEFAULT '' NOT NULL, + embed_art_path VARCHAR DEFAULT '' NOT NULL, + album_artist VARCHAR DEFAULT '' NOT NULL, + min_year SMALLINT DEFAULT 0 NOT NULL, + max_year SMALLINT DEFAULT 0 NOT NULL, + compilation BOOLEAN DEFAULT FALSE NOT NULL, + song_count INT DEFAULT 0 NOT NULL, + duration REAL DEFAULT 0 NOT NULL, + genre VARCHAR DEFAULT '' NOT NULL, + created_at TIMESTAMP, + updated_at TIMESTAMP, + full_text VARCHAR DEFAULT '', + album_artist_id VARCHAR DEFAULT '', + size BIGINT DEFAULT 0 NOT NULL, + description VARCHAR DEFAULT '' NOT NULL, + small_image_url VARCHAR DEFAULT '' NOT NULL, + medium_image_url VARCHAR DEFAULT '' NOT NULL, + large_image_url VARCHAR DEFAULT '' NOT NULL, + external_url VARCHAR DEFAULT '' NOT NULL, + external_info_updated_at TIMESTAMP, + date VARCHAR DEFAULT '' NOT NULL, + min_original_year SMALLINT DEFAULT 0 NOT NULL, + max_original_year SMALLINT DEFAULT 0 NOT NULL, + original_date VARCHAR DEFAULT '' NOT NULL, + release_date VARCHAR DEFAULT '' NOT NULL, + order_album_name VARCHAR DEFAULT '' NOT NULL, + order_album_artist_name VARCHAR DEFAULT '' NOT NULL, + sort_album_name VARCHAR DEFAULT '' NOT NULL, + sort_album_artist_name VARCHAR DEFAULT '' NOT NULL, + catalog_num VARCHAR DEFAULT '' NOT NULL, + comment VARCHAR DEFAULT '' NOT NULL, + mbz_album_id VARCHAR DEFAULT '' NOT NULL, + mbz_album_artist_id VARCHAR DEFAULT '' NOT NULL, + mbz_album_type VARCHAR DEFAULT '' NOT NULL, + mbz_album_comment VARCHAR DEFAULT '' NOT NULL, + discs JSONB DEFAULT '{}' NOT NULL, + library_id INT DEFAULT 1 NOT NULL REFERENCES library ON DELETE CASCADE, + imported_at TIMESTAMP DEFAULT '1970-01-01 00:00:00' NOT NULL, + missing BOOLEAN DEFAULT FALSE NOT NULL, + mbz_release_group_id VARCHAR DEFAULT '' NOT NULL, + tags JSONB DEFAULT '{}' NOT NULL, + participants JSONB DEFAULT '{}' NOT NULL, + folder_ids JSONB DEFAULT '[]' NOT NULL, + explicit_status VARCHAR DEFAULT '' NOT NULL +); + +CREATE INDEX album_alphabetical_by_artist ON album(compilation, order_album_artist_name, order_album_name); +CREATE INDEX album_artist_album_id ON album(album_artist_id); +CREATE INDEX album_created_at ON album(created_at); +CREATE INDEX album_genre ON album(genre); +CREATE INDEX album_max_year ON album(max_year); +CREATE INDEX album_mbz_album_type ON album(mbz_album_type); +CREATE INDEX album_min_year ON album(min_year); +CREATE INDEX album_name ON album(name); +CREATE INDEX album_order_album_artist_name ON album(order_album_artist_name); +CREATE INDEX album_order_album_name ON album(order_album_name); +CREATE INDEX album_size ON album(size); +CREATE INDEX album_sort_name ON album(COALESCE(NULLIF(sort_album_name, ''), order_album_name)); +CREATE INDEX album_sort_album_artist_name ON album(COALESCE(NULLIF(sort_album_artist_name, ''), order_album_artist_name)); +CREATE INDEX album_updated_at ON album(updated_at); +CREATE INDEX album_imported_at ON album(imported_at); +CREATE INDEX album_mbz_album_id ON album(mbz_album_id); +CREATE INDEX album_mbz_release_group_id ON album(mbz_release_group_id); + +-- Folder table +CREATE TABLE folder ( + id VARCHAR NOT NULL PRIMARY KEY, + library_id INT NOT NULL REFERENCES library(id) ON DELETE CASCADE, + path VARCHAR DEFAULT '' NOT NULL, + name VARCHAR DEFAULT '' NOT NULL, + missing BOOLEAN DEFAULT FALSE NOT NULL, + parent_id VARCHAR DEFAULT '' NOT NULL, + num_audio_files INT DEFAULT 0 NOT NULL, + num_playlists INT DEFAULT 0 NOT NULL, + image_files JSONB DEFAULT '[]' NOT NULL, + images_updated_at TIMESTAMP DEFAULT '1970-01-01 00:00:00' NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + hash VARCHAR DEFAULT '' NOT NULL +); + +CREATE INDEX folder_parent_id ON folder(parent_id); + +-- Media File table +CREATE TABLE media_file ( + id VARCHAR NOT NULL PRIMARY KEY, + path VARCHAR DEFAULT '' NOT NULL, + title VARCHAR DEFAULT '' NOT NULL, + album VARCHAR DEFAULT '' NOT NULL, + artist VARCHAR DEFAULT '' NOT NULL, + artist_id VARCHAR DEFAULT '' NOT NULL, + album_artist VARCHAR DEFAULT '' NOT NULL, + album_id VARCHAR DEFAULT '' NOT NULL, + has_cover_art BOOLEAN DEFAULT FALSE NOT NULL, + track_number SMALLINT DEFAULT 0 NOT NULL, + disc_number SMALLINT DEFAULT 0 NOT NULL, + year SMALLINT DEFAULT 0 NOT NULL, + size BIGINT DEFAULT 0 NOT NULL, + suffix VARCHAR DEFAULT '' NOT NULL, + duration REAL DEFAULT 0 NOT NULL, + bit_rate SMALLINT DEFAULT 0 NOT NULL, + genre VARCHAR DEFAULT '' NOT NULL, + compilation BOOLEAN DEFAULT FALSE NOT NULL, + created_at TIMESTAMP, + updated_at TIMESTAMP, + full_text VARCHAR DEFAULT '', + album_artist_id VARCHAR DEFAULT '', + date VARCHAR DEFAULT '' NOT NULL, + original_year SMALLINT DEFAULT 0 NOT NULL, + original_date VARCHAR DEFAULT '' NOT NULL, + release_year SMALLINT DEFAULT 0 NOT NULL, + release_date VARCHAR DEFAULT '' NOT NULL, + order_album_name VARCHAR DEFAULT '' NOT NULL, + order_album_artist_name VARCHAR DEFAULT '' NOT NULL, + order_artist_name VARCHAR DEFAULT '' NOT NULL, + sort_album_name VARCHAR DEFAULT '' NOT NULL, + sort_artist_name VARCHAR DEFAULT '' NOT NULL, + sort_album_artist_name VARCHAR DEFAULT '' NOT NULL, + sort_title VARCHAR DEFAULT '' NOT NULL, + disc_subtitle VARCHAR DEFAULT '' NOT NULL, + catalog_num VARCHAR DEFAULT '' NOT NULL, + comment VARCHAR DEFAULT '' NOT NULL, + order_title VARCHAR DEFAULT '' NOT NULL, + mbz_recording_id VARCHAR DEFAULT '' NOT NULL, + mbz_album_id VARCHAR DEFAULT '' NOT NULL, + mbz_artist_id VARCHAR DEFAULT '' NOT NULL, + mbz_album_artist_id VARCHAR DEFAULT '' NOT NULL, + mbz_album_type VARCHAR DEFAULT '' NOT NULL, + mbz_album_comment VARCHAR DEFAULT '' NOT NULL, + mbz_release_track_id VARCHAR DEFAULT '' NOT NULL, + bpm SMALLINT DEFAULT 0 NOT NULL, + channels SMALLINT DEFAULT 0 NOT NULL, + lyrics JSONB DEFAULT '[]' NOT NULL, + sample_rate INT DEFAULT 0 NOT NULL, + library_id INT DEFAULT 1 NOT NULL REFERENCES library ON DELETE CASCADE, + folder_id VARCHAR DEFAULT '' NOT NULL, + pid VARCHAR DEFAULT '' NOT NULL, + missing BOOLEAN DEFAULT FALSE NOT NULL, + mbz_release_group_id VARCHAR DEFAULT '' NOT NULL, + tags JSONB DEFAULT '{}' NOT NULL, + participants JSONB DEFAULT '{}' NOT NULL, + bit_depth SMALLINT DEFAULT 0 NOT NULL, + explicit_status VARCHAR DEFAULT '' NOT NULL, + birth_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + rg_album_gain REAL, + rg_album_peak REAL, + rg_track_gain REAL, + rg_track_peak REAL +); + +CREATE INDEX media_file_album_artist ON media_file(album_artist); +CREATE INDEX media_file_album_id ON media_file(album_id); +CREATE INDEX media_file_artist ON media_file(artist); +CREATE INDEX media_file_artist_album_id ON media_file(album_artist_id); +CREATE INDEX media_file_artist_id ON media_file(artist_id); +CREATE INDEX media_file_bpm ON media_file(bpm); +CREATE INDEX media_file_channels ON media_file(channels); +CREATE INDEX media_file_created_at ON media_file(created_at); +CREATE INDEX media_file_duration ON media_file(duration); +CREATE INDEX media_file_genre ON media_file(genre); +CREATE INDEX media_file_mbz_track_id ON media_file(mbz_recording_id); +CREATE INDEX media_file_order_album_name ON media_file(order_album_name); +CREATE INDEX media_file_order_artist_name ON media_file(order_artist_name); +CREATE INDEX media_file_order_title ON media_file(order_title); +CREATE INDEX media_file_path ON media_file(path); +CREATE INDEX media_file_sample_rate ON media_file(sample_rate); +CREATE INDEX media_file_sort_title ON media_file(COALESCE(NULLIF(sort_title, ''), order_title)); +CREATE INDEX media_file_sort_artist_name ON media_file(COALESCE(NULLIF(sort_artist_name, ''), order_artist_name)); +CREATE INDEX media_file_sort_album_name ON media_file(COALESCE(NULLIF(sort_album_name, ''), order_album_name)); +CREATE INDEX media_file_title ON media_file(title); +CREATE INDEX media_file_track_number ON media_file(disc_number, track_number); +CREATE INDEX media_file_updated_at ON media_file(updated_at); +CREATE INDEX media_file_year ON media_file(year); +CREATE INDEX media_file_birth_time ON media_file(birth_time); +CREATE INDEX media_file_folder_id ON media_file(folder_id); +CREATE INDEX media_file_pid ON media_file(pid); +CREATE INDEX media_file_missing ON media_file(missing); +CREATE INDEX media_file_mbz_release_track_id ON media_file(mbz_release_track_id); + +-- Media File Artists (many-to-many with roles) +CREATE TABLE media_file_artists ( + media_file_id VARCHAR NOT NULL REFERENCES media_file(id) ON DELETE CASCADE, + artist_id VARCHAR NOT NULL REFERENCES artist(id) ON DELETE CASCADE, + role VARCHAR DEFAULT '' NOT NULL, + sub_role VARCHAR DEFAULT '' NOT NULL, + CONSTRAINT artist_tracks UNIQUE (artist_id, media_file_id, role, sub_role) +); + +CREATE INDEX media_file_artists_media_file_id ON media_file_artists(media_file_id); +CREATE INDEX media_file_artists_role ON media_file_artists(role); + +-- Album Artists (many-to-many with roles) +CREATE TABLE album_artists ( + album_id VARCHAR NOT NULL REFERENCES album(id) ON DELETE CASCADE, + artist_id VARCHAR NOT NULL REFERENCES artist(id) ON DELETE CASCADE, + role VARCHAR DEFAULT '' NOT NULL, + sub_role VARCHAR DEFAULT '' NOT NULL, + CONSTRAINT album_artists_ux UNIQUE (album_id, artist_id, role, sub_role) +); + +CREATE INDEX album_artists_album_id ON album_artists(album_id); +CREATE INDEX album_artists_role ON album_artists(role); + +-- Playlist table +CREATE TABLE playlist ( + id VARCHAR NOT NULL PRIMARY KEY, + name VARCHAR DEFAULT '' NOT NULL, + comment VARCHAR DEFAULT '' NOT NULL, + duration REAL DEFAULT 0 NOT NULL, + song_count INT DEFAULT 0 NOT NULL, + public BOOLEAN DEFAULT FALSE NOT NULL, + created_at TIMESTAMP, + updated_at TIMESTAMP, + path VARCHAR DEFAULT '' NOT NULL, + sync BOOLEAN DEFAULT FALSE NOT NULL, + size BIGINT DEFAULT 0 NOT NULL, + rules VARCHAR, + evaluated_at TIMESTAMP, + owner_id VARCHAR NOT NULL REFERENCES "user"(id) ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE INDEX playlist_created_at ON playlist(created_at); +CREATE INDEX playlist_evaluated_at ON playlist(evaluated_at); +CREATE INDEX playlist_name ON playlist(name); +CREATE INDEX playlist_size ON playlist(size); +CREATE INDEX playlist_updated_at ON playlist(updated_at); + +-- Playlist Tracks table +CREATE TABLE playlist_tracks ( + id INT DEFAULT 0 NOT NULL, + playlist_id VARCHAR NOT NULL REFERENCES playlist(id) ON UPDATE CASCADE ON DELETE CASCADE, + media_file_id VARCHAR NOT NULL +); + +CREATE UNIQUE INDEX playlist_tracks_pos ON playlist_tracks(playlist_id, id); + +-- Playlist Fields table (for smart playlists) +CREATE TABLE playlist_fields ( + field VARCHAR NOT NULL, + playlist_id VARCHAR NOT NULL REFERENCES playlist(id) ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE UNIQUE INDEX playlist_fields_idx ON playlist_fields(field, playlist_id); + +-- Radio table +CREATE TABLE radio ( + id VARCHAR NOT NULL PRIMARY KEY, + name VARCHAR NOT NULL UNIQUE, + stream_url VARCHAR NOT NULL, + home_page_url VARCHAR DEFAULT '' NOT NULL, + created_at TIMESTAMP, + updated_at TIMESTAMP +); + +CREATE INDEX radio_name ON radio(name); + +-- Player table +CREATE TABLE player ( + id VARCHAR NOT NULL PRIMARY KEY, + name VARCHAR NOT NULL, + user_agent VARCHAR, + user_id VARCHAR NOT NULL REFERENCES "user"(id) ON UPDATE CASCADE ON DELETE CASCADE, + client VARCHAR NOT NULL, + ip VARCHAR, + last_seen TIMESTAMP, + max_bit_rate SMALLINT DEFAULT 0, + transcoding_id VARCHAR, + report_real_path BOOLEAN DEFAULT FALSE NOT NULL, + scrobble_enabled BOOLEAN DEFAULT TRUE +); + +CREATE INDEX player_match ON player(client, user_agent, user_id); +CREATE INDEX player_name ON player(name); + +-- Annotation table (play counts, ratings, stars) +CREATE TABLE annotation ( + user_id VARCHAR NOT NULL REFERENCES "user"(id) ON DELETE CASCADE ON UPDATE CASCADE, + item_id VARCHAR DEFAULT '' NOT NULL, + item_type VARCHAR DEFAULT '' NOT NULL, + play_count BIGINT DEFAULT 0, + play_date TIMESTAMP, + rating SMALLINT DEFAULT 0, + starred BOOLEAN DEFAULT FALSE NOT NULL, + starred_at TIMESTAMP, + UNIQUE (user_id, item_id, item_type) +); + +CREATE INDEX annotation_play_count ON annotation(play_count); +CREATE INDEX annotation_play_date ON annotation(play_date); +CREATE INDEX annotation_rating ON annotation(rating); +CREATE INDEX annotation_starred ON annotation(starred); +CREATE INDEX annotation_starred_at ON annotation(starred_at); + +-- Bookmark table +CREATE TABLE bookmark ( + user_id VARCHAR NOT NULL REFERENCES "user"(id) ON UPDATE CASCADE ON DELETE CASCADE, + item_id VARCHAR NOT NULL, + item_type VARCHAR NOT NULL, + comment VARCHAR, + position BIGINT, + changed_by VARCHAR, + created_at TIMESTAMP, + updated_at TIMESTAMP, + CONSTRAINT bookmark_pk UNIQUE (user_id, item_id, item_type) +); + +-- User Properties table +CREATE TABLE user_props ( + user_id VARCHAR NOT NULL REFERENCES "user"(id) ON UPDATE CASCADE ON DELETE CASCADE, + key VARCHAR NOT NULL, + value VARCHAR, + CONSTRAINT user_props_pk PRIMARY KEY (user_id, key) +); + +-- Scrobble Buffer table +CREATE TABLE scrobble_buffer ( + id VARCHAR NOT NULL DEFAULT '' PRIMARY KEY, + user_id VARCHAR NOT NULL REFERENCES "user"(id) ON UPDATE CASCADE ON DELETE CASCADE, + service VARCHAR NOT NULL, + media_file_id VARCHAR NOT NULL REFERENCES media_file(id) ON UPDATE CASCADE ON DELETE CASCADE, + play_time TIMESTAMP NOT NULL, + enqueue_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX scrobble_buffer_unique ON scrobble_buffer(user_id, service, media_file_id, play_time); + +-- Share table +CREATE TABLE share ( + id VARCHAR NOT NULL PRIMARY KEY, + expires_at TIMESTAMP, + last_visited_at TIMESTAMP, + resource_ids VARCHAR NOT NULL, + created_at TIMESTAMP, + updated_at TIMESTAMP, + user_id VARCHAR NOT NULL REFERENCES "user"(id) ON UPDATE CASCADE ON DELETE CASCADE, + downloadable BOOLEAN NOT NULL DEFAULT FALSE, + description VARCHAR NOT NULL DEFAULT '', + resource_type VARCHAR NOT NULL DEFAULT '', + contents VARCHAR NOT NULL DEFAULT '', + format VARCHAR NOT NULL DEFAULT '', + max_bit_rate SMALLINT NOT NULL DEFAULT 0, + visit_count BIGINT NOT NULL DEFAULT 0 +); + +-- Playqueue table +CREATE TABLE playqueue ( + id VARCHAR NOT NULL PRIMARY KEY, + user_id VARCHAR NOT NULL REFERENCES "user"(id) ON UPDATE CASCADE ON DELETE CASCADE, + current BIGINT NOT NULL DEFAULT 0, + position REAL, + changed_by VARCHAR, + items VARCHAR, + created_at TIMESTAMP, + updated_at TIMESTAMP +); + +-- Tag table +CREATE TABLE tag ( + id VARCHAR NOT NULL PRIMARY KEY, + tag_name VARCHAR DEFAULT '' NOT NULL, + tag_value VARCHAR DEFAULT '' NOT NULL, + CONSTRAINT tags_name_value UNIQUE (tag_name, tag_value) +); + +-- Library Tag table (tag statistics per library) +CREATE TABLE library_tag ( + tag_id VARCHAR NOT NULL REFERENCES tag(id) ON DELETE CASCADE, + library_id INT NOT NULL REFERENCES library(id) ON DELETE CASCADE, + album_count INT DEFAULT 0 NOT NULL, + media_file_count BIGINT DEFAULT 0 NOT NULL, + PRIMARY KEY (tag_id, library_id) +); + +CREATE INDEX idx_library_tag_tag_id ON library_tag(tag_id); +CREATE INDEX idx_library_tag_library_id ON library_tag(library_id); + +-- User Library association table (many-to-many) +CREATE TABLE user_library ( + user_id VARCHAR NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, + library_id INT NOT NULL REFERENCES library(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, library_id) +); + +CREATE INDEX idx_user_library_user_id ON user_library(user_id); +CREATE INDEX idx_user_library_library_id ON user_library(library_id); + +-- Insert default library +INSERT INTO library (id, name, path, default_new_users) +VALUES (1, 'Music Library', './music', true); + +-- +goose Down +SELECT 1; diff --git a/db/migrations/20251101180108_add_media_file_artists_artist_id_index.sql b/db/migrations/20251101180108_add_media_file_artists_artist_id_index.sql new file mode 100644 index 000000000..f3162ff59 --- /dev/null +++ b/db/migrations/20251101180108_add_media_file_artists_artist_id_index.sql @@ -0,0 +1,7 @@ +-- +goose Up +-- Add index on artist_id for media_file_artists table to improve query performance +-- This index is crucial for the RefreshStats query in artistRepository +CREATE INDEX IF NOT EXISTS media_file_artists_artist_id ON media_file_artists(artist_id); + +-- +goose Down +DROP INDEX IF EXISTS media_file_artists_artist_id; diff --git a/db/migrations/migration.go b/db/migrations/migration.go index 8d8f8a91e..735b8fde0 100644 --- a/db/migrations/migration.go +++ b/db/migrations/migration.go @@ -78,43 +78,3 @@ func createExecuteFunc(ctx context.Context, tx *sql.Tx) execStmtFunc { } } } - -// Hack way to add a new `not null` column to a table, setting the initial value for existing rows based on a -// SQL expression. It is done in 3 steps: -// 1. Add the column as nullable. Due to the way SQLite manipulates the DDL in memory, we need to add extra padding -// to the default value to avoid truncating it when changing the column to not null -// 2. Update the column with the initial value -// 3. Change the column to not null with the default value -// -// Based on https://stackoverflow.com/a/25917323 -func createAddColumnFunc(ctx context.Context, tx *sql.Tx) addColumnFunc { - return func(tableName, columnName, columnType, defaultValue, initialValue string) execFunc { - return func() error { - // Format the `default null` value to have the same length as the final defaultValue - finalLen := len(fmt.Sprintf(`%s not`, defaultValue)) - tempDefault := fmt.Sprintf(`default %s null`, strings.Repeat(" ", finalLen)) - _, err := tx.ExecContext(ctx, fmt.Sprintf(` -alter table %s add column %s %s %s;`, tableName, columnName, columnType, tempDefault)) - if err != nil { - return err - } - _, err = tx.ExecContext(ctx, fmt.Sprintf(` -update %s set %s = %s where %[2]s is null;`, tableName, columnName, initialValue)) - if err != nil { - return err - } - _, err = tx.ExecContext(ctx, fmt.Sprintf(` -PRAGMA writable_schema = on; -UPDATE sqlite_master -SET sql = replace(sql, '%[1]s %[2]s %[5]s', '%[1]s %[2]s default %[3]s not null') -WHERE type = 'table' - AND name = '%[4]s'; -PRAGMA writable_schema = off; -`, columnName, columnType, defaultValue, tableName, tempDefault)) - if err != nil { - return err - } - return err - } - } -} diff --git a/go.mod b/go.mod index 265cbfa6d..a7fa3ada0 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/djherbis/times v1.6.0 github.com/dustin/go-humanize v1.0.1 github.com/fatih/structs v1.1.0 + github.com/fergusstrange/embedded-postgres v1.32.0 github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/cors v1.2.2 github.com/go-chi/httprate v0.15.0 @@ -33,6 +34,7 @@ require ( github.com/google/wire v0.7.0 github.com/gorilla/websocket v1.5.3 github.com/hashicorp/go-multierror v1.1.1 + github.com/jackc/pgx/v5 v5.7.6 github.com/jellydator/ttlcache/v3 v3.4.0 github.com/kardianos/service v1.2.4 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 @@ -40,7 +42,6 @@ require ( github.com/kr/pretty v0.3.1 github.com/lestrrat-go/jwx/v2 v2.1.6 github.com/matoous/go-nanoid/v2 v2.1.0 - github.com/mattn/go-sqlite3 v1.14.32 github.com/microcosm-cc/bluemonday v1.0.27 github.com/mileusna/useragent v1.3.5 github.com/onsi/ginkgo/v2 v2.27.1 @@ -94,6 +95,9 @@ require ( github.com/gorilla/css v1.0.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect @@ -103,6 +107,7 @@ require ( github.com/lestrrat-go/httprc v1.0.6 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -123,6 +128,7 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/go.sum b/go.sum index f9e620fb2..de0667d34 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fergusstrange/embedded-postgres v1.32.0 h1:kh2ozEvAx2A0LoIJZEGNwHmoFTEQD243KrHjifcYGMo= +github.com/fergusstrange/embedded-postgres v1.32.0/go.mod h1:w0YvnCgf19o6tskInrOOACtnqfVlOvluz3hlNLY7tRk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -120,6 +122,14 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= +github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY= github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= @@ -162,14 +172,14 @@ github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVf github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE= github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= -github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= @@ -259,6 +269,7 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -279,6 +290,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU= github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/model/criteria/criteria_suite_test.go b/model/criteria/criteria_suite_test.go index 36e74cfa4..9fddf1707 100644 --- a/model/criteria/criteria_suite_test.go +++ b/model/criteria/criteria_suite_test.go @@ -3,7 +3,7 @@ package criteria import ( "testing" - _ "github.com/mattn/go-sqlite3" + //_ "github.com/mattn/go-sqlite3" "github.com/navidrome/navidrome/log" . "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" diff --git a/model/metadata/metadata_suite_test.go b/model/metadata/metadata_suite_test.go index fc299c7e9..4fb05ec45 100644 --- a/model/metadata/metadata_suite_test.go +++ b/model/metadata/metadata_suite_test.go @@ -6,7 +6,7 @@ import ( "time" "github.com/djherbis/times" - _ "github.com/mattn/go-sqlite3" + //_ "github.com/mattn/go-sqlite3" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/tests" . "github.com/onsi/ginkgo/v2" diff --git a/model/model_suite_test.go b/model/model_suite_test.go index 39184edc1..c9eb478ba 100644 --- a/model/model_suite_test.go +++ b/model/model_suite_test.go @@ -3,7 +3,7 @@ package model_test import ( "testing" - _ "github.com/mattn/go-sqlite3" + //_ "github.com/mattn/go-sqlite3" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/tests" . "github.com/onsi/ginkgo/v2" diff --git a/persistence/album_repository.go b/persistence/album_repository.go index 6f9bb3b48..441e5d850 100644 --- a/persistence/album_repository.go +++ b/persistence/album_repository.go @@ -219,7 +219,7 @@ func (r *albumRepository) UpdateExternalInfo(al *model.Album) error { func (r *albumRepository) selectAlbum(options ...model.QueryOptions) SelectBuilder { sql := r.newSelect(options...).Columns("album.*", "library.path as library_path", "library.name as library_name"). - LeftJoin("library on album.library_id = library.id") + LeftJoin("library on album.library_id = library.id").GroupBy("library_path", "library_name") sql = r.withAnnotation(sql, "album.id") return r.applyLibraryFilter(sql) } diff --git a/persistence/artist_repository.go b/persistence/artist_repository.go index 6d08c27db..7e4c0dfca 100644 --- a/persistence/artist_repository.go +++ b/persistence/artist_repository.go @@ -187,8 +187,7 @@ func (r *artistRepository) applyLibraryFilterToArtistQuery(query SelectBuilder) func (r *artistRepository) selectArtist(options ...model.QueryOptions) SelectBuilder { // Stats Format: {"1": {"albumartist": {"m": 10, "a": 5, "s": 1024}, "artist": {...}}, "2": {...}} query := r.newSelect(options...).Columns("artist.*", - "JSON_GROUP_OBJECT(library_artist.library_id, JSONB(library_artist.stats)) as library_stats_json") - + "JSON_OBJECT_AGG(library_artist.library_id, library_artist.stats::jsonb) as library_stats_json") query = r.applyLibraryFilterToArtistQuery(query) query = query.GroupBy("artist.id") return r.withAnnotation(query, "artist.id") @@ -348,12 +347,15 @@ set missing = (artist.id not in (select artist_id from artists_with_non_missing_ func (r *artistRepository) RefreshPlayCounts() (int64, error) { query := Expr(` with play_counts as ( - select user_id, atom as artist_id, sum(play_count) as total_play_count, max(play_date) as last_play_date + select user_id, + (elem->>'id')::text as artist_id, + sum(play_count) as total_play_count, + max(play_date) as last_play_date from media_file join annotation on item_id = media_file.id - left join json_tree(participants, '$.artist') as jt - where atom is not null and key = 'id' - group by user_id, atom + cross join jsonb_array_elements(participants->'artist') as elem + where elem->>'id' is not null + group by user_id, elem->>'id' ) insert into annotation (user_id, item_id, item_type, play_count, play_date) select user_id, artist_id, 'artist', total_play_count, last_play_date @@ -361,7 +363,7 @@ from play_counts where total_play_count > 0 on conflict (user_id, item_id, item_type) do update set play_count = excluded.play_count, - play_date = excluded.play_date; + play_date = excluded.play_date `) return r.executeSQL(query) } @@ -397,7 +399,7 @@ func (r *artistRepository) RefreshStats(allArtists bool) (int64, error) { } // Template for the batch update with placeholder markers that we'll replace - // This now calculates per-library statistics and stores them in library_artist.stats + // This calculates per-library statistics and stores them in library_artist.stats batchUpdateStatsSQL := ` WITH artist_role_counters AS ( SELECT mfa.artist_id, @@ -446,9 +448,9 @@ func (r *artistRepository) RefreshStats(allArtists bool) (int64, error) { library_artist_counters AS ( SELECT artist_id, library_id, - json_group_object( + jsonb_object_agg( role, - json_object('a', album_count, 'm', count, 's', size) + jsonb_build_object('a', album_count, 'm', count, 's', size) ) AS counters FROM combined_counters GROUP BY artist_id, library_id @@ -456,7 +458,7 @@ func (r *artistRepository) RefreshStats(allArtists bool) (int64, error) { UPDATE library_artist SET stats = coalesce((SELECT counters FROM library_artist_counters lac WHERE lac.artist_id = library_artist.artist_id - AND lac.library_id = library_artist.library_id), '{}') + AND lac.library_id = library_artist.library_id), '{}'::jsonb) WHERE library_artist.artist_id IN (ROLE_IDS_PLACEHOLDER);` // Will replace with actual placeholders var totalRowsAffected int64 = 0 diff --git a/persistence/helpers.go b/persistence/helpers.go index 73815ae45..48de94c5f 100644 --- a/persistence/helpers.go +++ b/persistence/helpers.go @@ -87,6 +87,6 @@ var sortOrderRegex = regexp.MustCompile(`order_([a-z_]+)`) // It finds order column names anywhere in the substring func mapSortOrder(tableName, order string) string { order = strings.ToLower(order) - repl := fmt.Sprintf("(coalesce(nullif(%[1]s.sort_$1,''),%[1]s.order_$1) collate nocase)", tableName) + repl := fmt.Sprintf("(coalesce(nullif(%[1]s.sort_$1,''),%[1]s.order_$1)::citext)", tableName) return sortOrderRegex.ReplaceAllString(order, repl) } diff --git a/persistence/library_repository.go b/persistence/library_repository.go index 314b682bb..975b8b445 100644 --- a/persistence/library_repository.go +++ b/persistence/library_repository.go @@ -117,7 +117,7 @@ func (r *libraryRepository) Put(l *model.Library) error { sql := Expr(` INSERT INTO user_library (user_id, library_id) SELECT u.id, l.id -FROM user u +FROM "user" u CROSS JOIN library l WHERE u.is_admin = true ON CONFLICT (user_id, library_id) DO NOTHING;`, @@ -173,11 +173,6 @@ func (r *libraryRepository) ScanEnd(id int) error { Set("last_scan_started_at", time.Time{}). Where(Eq{"id": id}) _, err := r.executeSQL(sq) - if err != nil { - return err - } - // https://www.sqlite.org/pragma.html#pragma_optimize - _, err = r.executeSQL(Expr("PRAGMA optimize=0x10012;")) return err } @@ -212,17 +207,17 @@ func (r *libraryRepository) RefreshStats(id int) error { }), &foldersRes) }, func() error { - return r.queryOne(Select("ifnull(sum(num_audio_files + num_playlists + json_array_length(image_files)),0) as count"). + return r.queryOne(Select("coalesce(sum(num_audio_files + num_playlists + jsonb_array_length(image_files)),0) as count"). From("folder").Where(Eq{"library_id": id, "missing": false}), &filesRes) }, func() error { return r.queryOne(Select("count(*) as count").From("media_file").Where(Eq{"library_id": id, "missing": true}), &missingRes) }, func() error { - return r.queryOne(Select("ifnull(sum(size),0) as sum").From("album").Where(Eq{"library_id": id, "missing": false}), &sizeRes) + return r.queryOne(Select("coalesce(sum(size),0) as sum").From("album").Where(Eq{"library_id": id, "missing": false}), &sizeRes) }, func() error { - return r.queryOne(Select("ifnull(sum(duration),0) as sum").From("album").Where(Eq{"library_id": id, "missing": false}), &durationRes) + return r.queryOne(Select("coalesce(sum(duration),0) as sum").From("album").Where(Eq{"library_id": id, "missing": false}), &durationRes) }, )() if err != nil { diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go index e7883947a..ae2fb67c6 100644 --- a/persistence/mediafile_repository.go +++ b/persistence/mediafile_repository.go @@ -137,7 +137,8 @@ func (r *mediaFileRepository) Put(m *model.MediaFile) error { func (r *mediaFileRepository) selectMediaFile(options ...model.QueryOptions) SelectBuilder { sql := r.newSelect(options...).Columns("media_file.*", "library.path as library_path", "library.name as library_name"). - LeftJoin("library on media_file.library_id = library.id") + LeftJoin("library on media_file.library_id = library.id"). + GroupBy("library.id") sql = r.withAnnotation(sql, "media_file.id") sql = r.withBookmark(sql, "media_file.id") return r.applyLibraryFilter(sql) @@ -193,7 +194,7 @@ func (r *mediaFileRepository) GetCursor(options ...model.QueryOptions) (model.Me } func (r *mediaFileRepository) FindByPaths(paths []string) (model.MediaFiles, error) { - sel := r.newSelect().Columns("*").Where(Eq{"path collate nocase": paths}) + sel := r.newSelect().Columns("*").Where(Eq{"path::citext": paths}) var res dbMediaFiles if err := r.queryAll(sel, &res); err != nil { return nil, err diff --git a/persistence/persistence_suite_test.go b/persistence/persistence_suite_test.go index 1007d84fe..fc3f6f9b8 100644 --- a/persistence/persistence_suite_test.go +++ b/persistence/persistence_suite_test.go @@ -5,7 +5,7 @@ import ( "path/filepath" "testing" - _ "github.com/mattn/go-sqlite3" + //_ "github.com/mattn/go-sqlite3" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/db" "github.com/navidrome/navidrome/log" diff --git a/persistence/player_repository.go b/persistence/player_repository.go index 73c820753..a2ad82b4c 100644 --- a/persistence/player_repository.go +++ b/persistence/player_repository.go @@ -35,8 +35,8 @@ func (r *playerRepository) Put(p *model.Player) error { func (r *playerRepository) selectPlayer(options ...model.QueryOptions) SelectBuilder { return r.newSelect(options...). Columns("player.*"). - Join("user ON player.user_id = user.id"). - Columns("user.user_name username") + Join(`"user" ON player.user_id = "user".id`). + Columns(`"user".user_name username`) } func (r *playerRepository) Get(id string) (*model.Player, error) { diff --git a/persistence/playlist_repository.go b/persistence/playlist_repository.go index 046284e1f..de40fef53 100644 --- a/persistence/playlist_repository.go +++ b/persistence/playlist_repository.go @@ -217,8 +217,8 @@ func (r *playlistRepository) GetPlaylists(mediaFileId string) (model.Playlists, } func (r *playlistRepository) selectPlaylist(options ...model.QueryOptions) SelectBuilder { - return r.newSelect(options...).Join("user on user.id = owner_id"). - Columns(r.tableName+".*", "user.user_name as owner_name") + return r.newSelect(options...).Join(`"user" on "user".id = owner_id`). + Columns(r.tableName+".*", `"user".user_name as owner_name`) } func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool { @@ -399,6 +399,7 @@ func (r *playlistRepository) loadTracks(sel SelectBuilder, id string) (model.Pla " AND annotation.user_id = '" + userID + "')"). Join("media_file f on f.id = media_file_id"). Join("library on f.library_id = library.id"). + GroupBy("library_path"). Where(Eq{"playlist_id": id}) tracks := dbPlaylistTracks{} err := r.queryAll(tracksQuery, &tracks) @@ -470,7 +471,7 @@ func (r *playlistRepository) removeOrphans() error { Join("playlist p on playlist_tracks.playlist_id = p.id"). LeftJoin("media_file mf on playlist_tracks.media_file_id = mf.id"). Where(Eq{"mf.id": nil}). - GroupBy("playlist_tracks.playlist_id") + GroupBy("playlist_tracks.playlist_id", "p.name") var pls []struct{ Id, Name string } err := r.queryAll(sel, &pls) diff --git a/persistence/playlist_track_repository.go b/persistence/playlist_track_repository.go index 01eec0d02..f46baa174 100644 --- a/persistence/playlist_track_repository.go +++ b/persistence/playlist_track_repository.go @@ -116,8 +116,9 @@ func (r *playlistTrackRepository) GetAll(options ...model.QueryOptions) (model.P } func (r *playlistTrackRepository) GetAlbumIDs(options ...model.QueryOptions) ([]string, error) { - query := r.newSelect(options...).Columns("distinct mf.album_id"). + query := r.newSelect(options...).Columns("mf.album_id"). Join("media_file mf on mf.id = media_file_id"). + GroupBy("mf.album_id"). Where(Eq{"playlist_id": r.playlistId}) var ids []string err := r.queryAllSlice(query, &ids) diff --git a/persistence/share_repository.go b/persistence/share_repository.go index d943943e0..b5af638eb 100644 --- a/persistence/share_repository.go +++ b/persistence/share_repository.go @@ -39,7 +39,7 @@ func (r *shareRepository) Delete(id string) error { } func (r *shareRepository) selectShare(options ...model.QueryOptions) SelectBuilder { - return r.newSelect(options...).Join("user u on u.id = share.user_id"). + return r.newSelect(options...).Join(`"user" u on u.id = share.user_id`). Columns("share.*", "user_name as username") } diff --git a/persistence/sql_annotations.go b/persistence/sql_annotations.go index 6691b553c..a8dd31268 100644 --- a/persistence/sql_annotations.go +++ b/persistence/sql_annotations.go @@ -24,10 +24,17 @@ func (r sqlRepository) withAnnotation(query SelectBuilder, idField string) Selec "annotation.item_id = "+idField+ " AND annotation.user_id = '"+userID+"')"). Columns( - "coalesce(starred, 0) as starred", + "coalesce(starred, false) as starred", "coalesce(rating, 0) as rating", "starred_at", - "play_date", + "play_date"). + GroupBy( + idField, + "annotation.starred", + "annotation.rating", + "annotation.starred_at", + "annotation.play_date", + "annotation.play_count", ) if conf.Server.AlbumPlayCountMode == consts.AlbumPlayCountModeNormalized && r.tableName == "album" { query = query.Columns( diff --git a/persistence/sql_base_repository.go b/persistence/sql_base_repository.go index ce026a3c3..081b9c2c4 100644 --- a/persistence/sql_base_repository.go +++ b/persistence/sql_base_repository.go @@ -13,6 +13,7 @@ import ( "time" . "github.com/Masterminds/squirrel" + "github.com/lann/builder" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" @@ -386,12 +387,20 @@ func (r sqlRepository) count(countQuery SelectBuilder, options ...model.QueryOpt countQuery = countQuery. RemoveColumns().Columns("count(distinct " + r.tableName + ".id) as count"). RemoveOffset().RemoveLimit(). - OrderBy(r.tableName + ".id"). // To remove any ORDER BY clause that could slow down the query + GroupBy(r.tableName + ".id"). From(r.tableName) + // To remove any ORDER BY clause that could slow down the query + countQuery = builder.Delete(countQuery, "OrderByParts").(SelectBuilder) countQuery = r.applyFilters(countQuery, options...) var res struct{ Count int64 } err := r.queryOne(countQuery, &res) - return res.Count, err + if errors.Is(err, model.ErrNotFound) { + return 0, nil + } + if err != nil { + return 0, err + } + return res.Count, nil } func (r sqlRepository) putByMatch(filter Sqlizer, id string, m interface{}, colsToUpdate ...string) (string, error) { diff --git a/persistence/sql_bookmarks.go b/persistence/sql_bookmarks.go index 52c4b8e9c..4c6d61811 100644 --- a/persistence/sql_bookmarks.go +++ b/persistence/sql_bookmarks.go @@ -23,6 +23,7 @@ func (r sqlRepository) withBookmark(query SelectBuilder, idField string) SelectB LeftJoin("bookmark on (" + "bookmark.item_id = " + idField + " AND bookmark.user_id = '" + userID + "')"). + GroupBy("bookmark.position"). Columns("coalesce(position, 0) as bookmark_position") } diff --git a/persistence/sql_participations.go b/persistence/sql_participations.go index d88eca45e..53550b0cb 100644 --- a/persistence/sql_participations.go +++ b/persistence/sql_participations.go @@ -80,18 +80,18 @@ func (r sqlRepository) updateParticipants(itemID string, participants model.Part // Build the INSERT query using json_each and INNER JOIN to artist table // to automatically filter out non-existent artist IDs query := fmt.Sprintf(` - INSERT INTO %[1]s_artists (%[1]s_id, artist_id, role, sub_role) - SELECT ?, - json_extract(value, '$.artist_id') as artist_id, - json_extract(value, '$.role') as role, - COALESCE(json_extract(value, '$.sub_role'), '') as sub_role - -- Parse the flat JSON array: [{"artist_id": "id", "role": "role", "sub_role": "subRole"}] - FROM json_each(?) -- Iterate through each array element - -- CRITICAL: Only insert records for artists that actually exist in the database - JOIN artist ON artist.id = json_extract(value, '$.artist_id') -- Filter out non-existent artist IDs via INNER JOIN - -- Handle duplicate insertions gracefully (e.g., if called multiple times) - ON CONFLICT (artist_id, %[1]s_id, role, sub_role) DO NOTHING -- Ignore duplicates - `, r.tableName) + INSERT INTO %[1]s_artists (%[1]s_id, artist_id, role, sub_role) + SELECT ?, + value->>'artist_id' as artist_id, + value->>'role' as role, + COALESCE(value->>'sub_role', '') as sub_role + -- Parse the flat JSON array: [{"artist_id": "id", "role": "role", "sub_role": "subRole"}] + FROM jsonb_array_elements(?::jsonb) -- Iterate through each array element + -- CRITICAL: Only insert records for artists that actually exist in the database + JOIN artist ON artist.id = value->>'artist_id' -- Filter out non-existent artist IDs via INNER JOIN + -- Handle duplicate insertions gracefully (e.g., if called multiple times) + ON CONFLICT (artist_id, %[1]s_id, role, sub_role) DO NOTHING -- Ignore duplicates +`, r.tableName) _, err = r.executeSQL(Expr(query, itemID, string(participantsJSON))) return err diff --git a/persistence/tag_repository.go b/persistence/tag_repository.go index b224450ab..643f03624 100644 --- a/persistence/tag_repository.go +++ b/persistence/tag_repository.go @@ -53,12 +53,13 @@ func (r *tagRepository) Add(libraryID int, tags ...model.Tag) error { func (r *tagRepository) UpdateCounts() error { template := ` INSERT INTO library_tag (tag_id, library_id, %[1]s_count) -SELECT jt.value as tag_id, %[1]s.library_id, count(distinct %[1]s.id) as %[1]s_count -FROM %[1]s -JOIN json_tree(%[1]s.tags, '$.genre') as jt ON jt.atom IS NOT NULL AND jt.key = 'id' -JOIN tag ON tag.id = jt.value -GROUP BY jt.value, %[1]s.library_id -ON CONFLICT (tag_id, library_id) +SELECT elem->>'id' as tag_id, %[1]s.library_id, count(distinct %[1]s.id) as %[1]s_count +FROM %[1]s, + LATERAL jsonb_array_elements(%[1]s.tags->'genre') AS elem +WHERE elem->>'id' IS NOT NULL + AND EXISTS (SELECT 1 FROM tag WHERE tag.id = elem->>'id') +GROUP BY elem->>'id', %[1]s.library_id +ON CONFLICT (tag_id, library_id) DO UPDATE SET %[1]s_count = excluded.%[1]s_count; ` @@ -76,16 +77,20 @@ DO UPDATE SET %[1]s_count = excluded.%[1]s_count; func (r *tagRepository) purgeUnused() error { del := Delete(r.tableName).Where(` - id not in (select jt.value - from album left join json_tree(album.tags, '$') as jt - where atom is not null - and key = 'id' - UNION - select jt.value - from media_file left join json_tree(media_file.tags, '$') as jt - where atom is not null - and key = 'id') -`) + id not in ( + select DISTINCT elem->>'id' + from album + cross join lateral jsonb_each(album.tags) as tag_type(key, value) + cross join lateral jsonb_array_elements(tag_type.value) as elem + where elem->>'id' is not null + UNION + select DISTINCT elem->>'id' + from media_file + cross join lateral jsonb_each(media_file.tags) as tag_type(key, value) + cross join lateral jsonb_array_elements(tag_type.value) as elem + where elem->>'id' is not null + ) + `) c, err := r.executeSQL(del) if err != nil { return fmt.Errorf("error purging unused tags: %w", err) diff --git a/persistence/user_repository.go b/persistence/user_repository.go index a7181b1a7..2c82fa786 100644 --- a/persistence/user_repository.go +++ b/persistence/user_repository.go @@ -55,7 +55,7 @@ func NewUserRepository(ctx context.Context, db dbx.Builder) model.UserRepository r := &userRepository{} r.ctx = ctx r.db = db - r.tableName = "user" + r.tableName = `"user"` r.registerModel(&model.User{}, map[string]filterFunc{ "password": invalidFilter(ctx), "name": r.withTableName(startsWithFilter), @@ -69,21 +69,26 @@ func NewUserRepository(ctx context.Context, db dbx.Builder) model.UserRepository // selectUserWithLibraries returns a SelectBuilder that includes library information func (r *userRepository) selectUserWithLibraries(options ...model.QueryOptions) SelectBuilder { return r.newSelect(options...). - Columns(`user.*`, - `COALESCE(json_group_array(json_object( - 'id', library.id, - 'name', library.name, - 'path', library.path, - 'remote_path', library.remote_path, - 'last_scan_at', library.last_scan_at, - 'last_scan_started_at', library.last_scan_started_at, - 'full_scan_in_progress', library.full_scan_in_progress, - 'updated_at', library.updated_at, - 'created_at', library.created_at - )) FILTER (WHERE library.id IS NOT NULL), '[]') AS libraries_json`). - LeftJoin("user_library ul ON user.id = ul.user_id"). + Columns(`"user".*`, + `COALESCE( + jsonb_agg( + jsonb_build_object( + 'id', library.id, + 'name', library.name, + 'path', library.path, + 'remote_path', library.remote_path, + 'last_scan_at', library.last_scan_at, + 'last_scan_started_at', library.last_scan_started_at, + 'full_scan_in_progress', library.full_scan_in_progress, + 'updated_at', library.updated_at, + 'created_at', library.created_at + ) + ) FILTER (WHERE library.id IS NOT NULL), + '[]'::jsonb + )::text AS libraries_json`). + LeftJoin(`user_library ul ON "user".id = ul.user_id`). LeftJoin("library ON ul.library_id = library.id"). - GroupBy("user.id") + GroupBy(`"user".id`) } func (r *userRepository) CountAll(qo ...model.QueryOptions) (int64, error) { @@ -91,7 +96,7 @@ func (r *userRepository) CountAll(qo ...model.QueryOptions) (int64, error) { } func (r *userRepository) Get(id string) (*model.User, error) { - sel := r.selectUserWithLibraries().Where(Eq{"user.id": id}) + sel := r.selectUserWithLibraries().Where(Eq{`"user".id`: id}) var res dbUser err := r.queryOne(sel, &res) if err != nil { @@ -144,7 +149,7 @@ func (r *userRepository) Put(u *model.User) error { // Auto-assign all libraries to admin users in a single SQL operation if u.IsAdmin { sql := Expr( - "INSERT OR IGNORE INTO user_library (user_id, library_id) SELECT ?, id FROM library", + "INSERT INTO user_library (user_id, library_id) SELECT ?, id FROM library ON CONFLICT DO NOTHING", u.ID, ) if _, err := r.executeSQL(sql); err != nil { @@ -153,7 +158,7 @@ func (r *userRepository) Put(u *model.User) error { } else if isNewUser { // Only for new regular users // Auto-assign default libraries to new regular users sql := Expr( - "INSERT OR IGNORE INTO user_library (user_id, library_id) SELECT ?, id FROM library WHERE default_new_users = true", + "INSERT INTO user_library (user_id, library_id) SELECT ?, id FROM library WHERE default_new_users = true ON CONFLICT DO NOTHING", u.ID, ) if _, err := r.executeSQL(sql); err != nil { @@ -165,7 +170,7 @@ func (r *userRepository) Put(u *model.User) error { } func (r *userRepository) FindFirstAdmin() (*model.User, error) { - sel := r.selectUserWithLibraries(model.QueryOptions{Sort: "updated_at", Max: 1}).Where(Eq{"user.is_admin": true}) + sel := r.selectUserWithLibraries(model.QueryOptions{Sort: "updated_at", Max: 1}).Where(Eq{`"user".is_admin`: true}) var usr dbUser err := r.queryOne(sel, &usr) if err != nil { @@ -175,7 +180,7 @@ func (r *userRepository) FindFirstAdmin() (*model.User, error) { } func (r *userRepository) FindByUsername(username string) (*model.User, error) { - sel := r.selectUserWithLibraries().Where(Expr("user.user_name = ? COLLATE NOCASE", username)) + sel := r.selectUserWithLibraries().Where(Expr(`"user".user_name = ?`, username)) var usr dbUser err := r.queryOne(sel, &usr) if err != nil { diff --git a/scanner/scanner.go b/scanner/scanner.go index 04a5c2456..108224356 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -11,7 +11,6 @@ import ( "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/artwork" - "github.com/navidrome/navidrome/db" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/utils/run" @@ -181,9 +180,7 @@ func (s *scannerImpl) runRefreshStats(ctx context.Context, state *scanState) fun func (s *scannerImpl) runOptimize(ctx context.Context) func() error { return func() error { - start := time.Now() - db.Optimize(ctx) - log.Debug(ctx, "Scanner: Optimized DB", "elapsed", time.Since(start)) + // TODO: Noop for now return nil } } @@ -210,10 +207,11 @@ func (s *scannerImpl) runUpdateLibraries(ctx context.Context, state *scanState) } if state.changesDetected.Load() { log.Debug(ctx, "Scanner: Refreshing library stats", "lib", lib.Name) - if err := tx.Library(ctx).RefreshStats(lib.ID); err != nil { - log.Error(ctx, "Scanner: Error refreshing library stats", "lib", lib.Name, err) - return fmt.Errorf("refreshing library stats: %w", err) - } + // FIXME + // if err := tx.Library(ctx).RefreshStats(lib.ID); err != nil { + // log.Error(ctx, "Scanner: Error refreshing library stats", "lib", lib.Name, err) + // return fmt.Errorf("refreshing library stats: %w", err) + // } } else { log.Debug(ctx, "Scanner: No changes detected, skipping library stats refresh", "lib", lib.Name) } diff --git a/ui/package-lock.json b/ui/package-lock.json index c0901a73d..91fdd33be 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -120,6 +120,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1616,6 +1617,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" }, @@ -1638,6 +1640,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" } @@ -2284,6 +2287,7 @@ "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.4.tgz", "integrity": "sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ==", "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.", + "peer": true, "dependencies": { "@babel/runtime": "^7.4.4", "@material-ui/styles": "^4.11.5", @@ -2328,6 +2332,7 @@ "version": "4.11.3", "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.3.tgz", "integrity": "sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA==", + "peer": true, "dependencies": { "@babel/runtime": "^7.4.4" }, @@ -2959,6 +2964,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz", "integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==", + "peer": true, "dependencies": { "@types/react": "*", "hoist-non-react-statics": "^3.3.0" @@ -3005,6 +3011,7 @@ "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3024,6 +3031,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.89.tgz", "integrity": "sha512-I98SaDCar5lvEYl80ClRIUztH/hyWHR+I2f+5yTVp/MQ205HgYkA2b5mVdry/+nsEIrf8I65KA5V/PASx68MsQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "^0.16", @@ -3155,6 +3163,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -3470,6 +3479,7 @@ "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4020,6 +4030,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", @@ -4431,6 +4442,7 @@ "version": "6.9.3", "resolved": "https://registry.npmjs.org/connected-react-router/-/connected-react-router-6.9.3.tgz", "integrity": "sha512-4ThxysOiv/R2Dc4Cke1eJwjKwH1Y51VDwlOrOfs1LjpdYOVvCNjNkZDayo7+sx42EeGJPQUNchWkjAIJdXGIOQ==", + "peer": true, "dependencies": { "lodash.isequalwith": "^4.4.0", "prop-types": "^15.7.2" @@ -5257,6 +5269,7 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5817,6 +5830,7 @@ "version": "4.20.10", "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.10.tgz", "integrity": "sha512-TL48Pi1oNHeMOHrKv1bCJUrWZDcD3DIG6AGYVNOnyZPr7Bd/pStN0pL+lfzF5BNoj/FclaoiaLenk4XUIFVYng==", + "peer": true, "dependencies": { "@babel/runtime": "^7.10.0" }, @@ -5832,6 +5846,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/final-form-arrays/-/final-form-arrays-3.1.0.tgz", "integrity": "sha512-TWBvun+AopgBLw9zfTFHBllnKMVNEwCEyDawphPuBGGqNsuhGzhT7yewHys64KFFwzIs6KEteGLpKOwvTQEscQ==", + "peer": true, "peerDependencies": { "final-form": "^4.20.8" } @@ -6318,6 +6333,7 @@ "version": "4.10.1", "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "peer": true, "dependencies": { "@babel/runtime": "^7.1.2", "loose-envify": "^1.2.0", @@ -8381,6 +8397,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -8465,6 +8482,7 @@ "version": "3.19.12", "resolved": "https://registry.npmjs.org/ra-core/-/ra-core-3.19.12.tgz", "integrity": "sha512-E0cM6OjEUtccaR+dR5mL1MLiVVYML0Yf7aPhpLEq4iue73X3+CKcLztInoBhWgeevPbFQwgAtsXhlpedeyrNNg==", + "peer": true, "dependencies": { "classnames": "~2.3.1", "date-fns": "^1.29.0", @@ -8821,6 +8839,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -8898,6 +8917,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -8973,6 +8993,7 @@ "version": "6.5.9", "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.9.tgz", "integrity": "sha512-x3XYvozolECp3nIjly+4QqxdjSSWfcnpGEL5K8OBT6xmGrq5kBqbA6+/tOqoom9NwqIPPbxPNsOViFlbKgowbA==", + "peer": true, "dependencies": { "@babel/runtime": "^7.15.4" }, @@ -8989,6 +9010,7 @@ "version": "3.1.4", "resolved": "https://registry.npmjs.org/react-final-form-arrays/-/react-final-form-arrays-3.1.4.tgz", "integrity": "sha512-siVFAolUAe29rMR6u8VwepoysUcUdh6MLV2OWnCtKpsPRUdT9VUgECjAPaVMAH2GROZNiVB9On1H9MMrm9gdpg==", + "peer": true, "dependencies": { "@babel/runtime": "^7.19.4" }, @@ -9085,6 +9107,7 @@ "version": "7.2.9", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "peer": true, "dependencies": { "@babel/runtime": "^7.15.4", "@types/react-redux": "^7.1.20", @@ -9118,6 +9141,7 @@ "version": "5.3.4", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -9137,6 +9161,7 @@ "version": "5.3.4", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -9304,6 +9329,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "peer": true, "dependencies": { "@babel/runtime": "^7.9.2" } @@ -9313,6 +9339,7 @@ "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.4.2.tgz", "integrity": "sha512-QLIn/q+7MX/B+MkGJ/K6R3//60eJ4QNy65eqPsJrfGezbxdh1Jx+37VRKE2K4PsJnNET5JufJtgWdT30WBa+6w==", "license": "MIT", + "peer": true, "dependencies": { "@redux-saga/core": "^1.4.2" } @@ -9549,6 +9576,7 @@ "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.41.1.tgz", "integrity": "sha512-70qfem+U3hAgwNgOlnUQiIdfKHLELUxsEWbFWg3aErPUvsyXYF1HALJBwoDgMUhRWyn+SqWVneDTnO/Kbey9hg==", "devOptional": true, + "peer": true, "dependencies": { "@types/estree": "1.0.7" }, @@ -10395,6 +10423,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -10614,6 +10643,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10858,6 +10888,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -10979,6 +11010,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -10991,6 +11023,7 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.3.tgz", "integrity": "sha512-IUSop8jgaT7w0g1yOM/35qVtKjr/8Va4PrjzH1OUb0YH4c3OXB2lCZDkMAB6glA8T5w8S164oJGsbcmAecr4sA==", "dev": true, + "peer": true, "dependencies": { "@vitest/expect": "4.0.3", "@vitest/mocker": "4.0.3", @@ -11422,6 +11455,7 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -11466,6 +11500,7 @@ "version": "2.79.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "peer": true, "bin": { "rollup": "dist/bin/rollup" },