@@ -9,8 +9,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"gopkg.in/gormigrate.v1"
|
||||
)
|
||||
|
||||
func DefaultOptions() url.Values {
|
||||
@@ -49,30 +47,6 @@ func New(path string, options url.Values) (*DB, error) {
|
||||
}
|
||||
db.SetLogger(log.New(os.Stdout, "gorm ", 0))
|
||||
db.DB().SetMaxOpenConns(1)
|
||||
migrOptions := &gormigrate.Options{
|
||||
TableName: "migrations",
|
||||
IDColumnName: "id",
|
||||
IDColumnSize: 255,
|
||||
UseTransaction: false,
|
||||
}
|
||||
migr := gormigrate.New(db, migrOptions, wrapMigrations(
|
||||
migrateInitSchema(),
|
||||
migrateCreateInitUser(),
|
||||
migrateMergePlaylist(),
|
||||
migrateCreateTranscode(),
|
||||
migrateAddGenre(),
|
||||
migrateUpdateTranscodePrefIDX(),
|
||||
migrateAddAlbumIDX(),
|
||||
migrateMultiGenre(),
|
||||
migrateListenBrainz(),
|
||||
migratePodcast(),
|
||||
migrateBookmarks(),
|
||||
migratePodcastAutoDownload(),
|
||||
migrateAlbumCreatedAt(),
|
||||
))
|
||||
if err = migr.Migrate(); err != nil {
|
||||
return nil, fmt.Errorf("migrating to latest version: %w", err)
|
||||
}
|
||||
return &DB{DB: db}, nil
|
||||
}
|
||||
|
||||
|
||||
+262
-234
@@ -3,33 +3,45 @@ package db
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"gopkg.in/gormigrate.v1"
|
||||
)
|
||||
|
||||
// $ date '+%Y%m%d%H%M'
|
||||
type MigrationContext struct {
|
||||
OriginalMusicPath string
|
||||
}
|
||||
|
||||
func migrateInitSchema() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202002192100",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
Genre{},
|
||||
TrackGenre{},
|
||||
AlbumGenre{},
|
||||
Track{},
|
||||
Artist{},
|
||||
User{},
|
||||
Setting{},
|
||||
Play{},
|
||||
Album{},
|
||||
Playlist{},
|
||||
PlayQueue{},
|
||||
).
|
||||
Error
|
||||
},
|
||||
func (db *DB) Migrate(ctx MigrationContext) error {
|
||||
options := &gormigrate.Options{
|
||||
TableName: "migrations",
|
||||
IDColumnName: "id",
|
||||
IDColumnSize: 255,
|
||||
UseTransaction: false,
|
||||
}
|
||||
|
||||
// $ date '+%Y%m%d%H%M'
|
||||
migrations := []*gormigrate.Migration{
|
||||
construct(ctx, "202002192100", migrateInitSchema),
|
||||
construct(ctx, "202002192019", migrateCreateInitUser),
|
||||
construct(ctx, "202002192222", migrateMergePlaylist),
|
||||
construct(ctx, "202003111222", migrateCreateTranscode),
|
||||
construct(ctx, "202003121330", migrateAddGenre),
|
||||
construct(ctx, "202003241509", migrateUpdateTranscodePrefIDX),
|
||||
construct(ctx, "202004302006", migrateAddAlbumIDX),
|
||||
construct(ctx, "202012151806", migrateMultiGenre),
|
||||
construct(ctx, "202101081149", migrateListenBrainz),
|
||||
construct(ctx, "202101111537", migratePodcast),
|
||||
construct(ctx, "202102032210", migrateBookmarks),
|
||||
construct(ctx, "202102191448", migratePodcastAutoDownload),
|
||||
construct(ctx, "202110041330", migrateAlbumCreatedAt),
|
||||
construct(ctx, "202111021951", migrateAlbumRootDir),
|
||||
}
|
||||
|
||||
return gormigrate.
|
||||
New(db.DB, options, migrations).
|
||||
Migrate()
|
||||
}
|
||||
|
||||
func construct(ctx MigrationContext, id string, f func(*gorm.DB, MigrationContext) error) *gormigrate.Migration {
|
||||
@@ -41,240 +53,256 @@ func construct(ctx MigrationContext, id string, f func(*gorm.DB, MigrationContex
|
||||
if err := f(tx, ctx); err != nil {
|
||||
return fmt.Errorf("%q: %w", id, err)
|
||||
}
|
||||
|
||||
return tx.Create(&User{
|
||||
Name: initUsername,
|
||||
Password: initPassword,
|
||||
IsAdmin: true,
|
||||
}).
|
||||
Error
|
||||
log.Printf("migration '%s' finished", id)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateMergePlaylist() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202002192222",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if !tx.HasTable("playlist_items") {
|
||||
return nil
|
||||
}
|
||||
|
||||
return tx.Exec(`
|
||||
UPDATE playlists
|
||||
SET items=( SELECT group_concat(track_id) FROM (
|
||||
SELECT track_id
|
||||
FROM playlist_items
|
||||
WHERE playlist_items.playlist_id=playlists.id
|
||||
ORDER BY created_at
|
||||
) );
|
||||
DROP TABLE playlist_items;`,
|
||||
).
|
||||
Error
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateCreateTranscode() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202003111222",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
TranscodePreference{},
|
||||
).
|
||||
Error
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateAddGenre() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202003121330",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
Genre{},
|
||||
Album{},
|
||||
Track{},
|
||||
).
|
||||
Error
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateUpdateTranscodePrefIDX() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202003241509",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
var hasIDX int
|
||||
tx.
|
||||
Select("1").
|
||||
Table("sqlite_master").
|
||||
Where("type = ?", "index").
|
||||
Where("name = ?", "idx_user_id_client").
|
||||
Count(&hasIDX)
|
||||
if hasIDX == 1 {
|
||||
// index already exists
|
||||
return nil
|
||||
}
|
||||
|
||||
step := tx.Exec(`
|
||||
ALTER TABLE transcode_preferences RENAME TO transcode_preferences_orig;
|
||||
`)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step rename: %w", err)
|
||||
}
|
||||
|
||||
step = tx.AutoMigrate(
|
||||
TranscodePreference{},
|
||||
)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step create: %w", err)
|
||||
}
|
||||
|
||||
step = tx.Exec(`
|
||||
INSERT INTO transcode_preferences (user_id, client, profile)
|
||||
SELECT user_id, client, profile
|
||||
FROM transcode_preferences_orig;
|
||||
DROP TABLE transcode_preferences_orig;
|
||||
`)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step copy: %w", err)
|
||||
}
|
||||
Rollback: func(*gorm.DB) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateAddAlbumIDX() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202004302006",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
Album{},
|
||||
).
|
||||
Error
|
||||
},
|
||||
}
|
||||
func migrateInitSchema(tx *gorm.DB, _ MigrationContext) error {
|
||||
return tx.AutoMigrate(
|
||||
Genre{},
|
||||
TrackGenre{},
|
||||
AlbumGenre{},
|
||||
Track{},
|
||||
Artist{},
|
||||
User{},
|
||||
Setting{},
|
||||
Play{},
|
||||
Album{},
|
||||
Playlist{},
|
||||
PlayQueue{},
|
||||
).
|
||||
Error
|
||||
}
|
||||
|
||||
func migrateMultiGenre() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202012151806",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
step := tx.AutoMigrate(
|
||||
Genre{},
|
||||
TrackGenre{},
|
||||
AlbumGenre{},
|
||||
Track{},
|
||||
Album{},
|
||||
)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step auto migrate: %w", err)
|
||||
}
|
||||
|
||||
var genreCount int
|
||||
tx.
|
||||
Model(Genre{}).
|
||||
Count(&genreCount)
|
||||
if genreCount == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
step = tx.Exec(`
|
||||
INSERT INTO track_genres (track_id, genre_id)
|
||||
SELECT id, tag_genre_id
|
||||
FROM tracks
|
||||
WHERE tag_genre_id IS NOT NULL;
|
||||
UPDATE tracks SET tag_genre_id=NULL;
|
||||
`)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step migrate track genres: %w", err)
|
||||
}
|
||||
|
||||
step = tx.Exec(`
|
||||
INSERT INTO album_genres (album_id, genre_id)
|
||||
SELECT id, tag_genre_id
|
||||
FROM albums
|
||||
WHERE tag_genre_id IS NOT NULL;
|
||||
UPDATE albums SET tag_genre_id=NULL;
|
||||
`)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step migrate album genres: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func migrateCreateInitUser(tx *gorm.DB, _ MigrationContext) error {
|
||||
const (
|
||||
initUsername = "admin"
|
||||
initPassword = "admin"
|
||||
)
|
||||
err := tx.
|
||||
Where("name=?", initUsername).
|
||||
First(&User{}).
|
||||
Error
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return tx.Create(&User{
|
||||
Name: initUsername,
|
||||
Password: initPassword,
|
||||
IsAdmin: true,
|
||||
}).
|
||||
Error
|
||||
}
|
||||
|
||||
func migrateListenBrainz() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202101081149",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
step := tx.AutoMigrate(
|
||||
User{},
|
||||
)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step auto migrate: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func migrateMergePlaylist(tx *gorm.DB, _ MigrationContext) error {
|
||||
if !tx.HasTable("playlist_items") {
|
||||
return nil
|
||||
}
|
||||
|
||||
return tx.Exec(`
|
||||
UPDATE playlists
|
||||
SET items=( SELECT group_concat(track_id) FROM (
|
||||
SELECT track_id
|
||||
FROM playlist_items
|
||||
WHERE playlist_items.playlist_id=playlists.id
|
||||
ORDER BY created_at
|
||||
) );
|
||||
DROP TABLE playlist_items;`,
|
||||
).
|
||||
Error
|
||||
}
|
||||
|
||||
func migratePodcast() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202101111537",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
Podcast{},
|
||||
PodcastEpisode{},
|
||||
).
|
||||
Error
|
||||
},
|
||||
}
|
||||
func migrateCreateTranscode(tx *gorm.DB, _ MigrationContext) error {
|
||||
return tx.AutoMigrate(
|
||||
TranscodePreference{},
|
||||
).
|
||||
Error
|
||||
}
|
||||
|
||||
func migrateBookmarks() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202102032210",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
Bookmark{},
|
||||
).
|
||||
Error
|
||||
},
|
||||
}
|
||||
func migrateAddGenre(tx *gorm.DB, _ MigrationContext) error {
|
||||
return tx.AutoMigrate(
|
||||
Genre{},
|
||||
Album{},
|
||||
Track{},
|
||||
).
|
||||
Error
|
||||
}
|
||||
|
||||
func migratePodcastAutoDownload() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202102191448",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
Podcast{},
|
||||
).
|
||||
Error
|
||||
},
|
||||
func migrateUpdateTranscodePrefIDX(tx *gorm.DB, _ MigrationContext) error {
|
||||
var hasIDX int
|
||||
tx.
|
||||
Select("1").
|
||||
Table("sqlite_master").
|
||||
Where("type = ?", "index").
|
||||
Where("name = ?", "idx_user_id_client").
|
||||
Count(&hasIDX)
|
||||
if hasIDX == 1 {
|
||||
// index already exists
|
||||
return nil
|
||||
}
|
||||
|
||||
step := tx.Exec(`
|
||||
ALTER TABLE transcode_preferences RENAME TO transcode_preferences_orig;
|
||||
`)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step rename: %w", err)
|
||||
}
|
||||
|
||||
step = tx.AutoMigrate(
|
||||
TranscodePreference{},
|
||||
)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step create: %w", err)
|
||||
}
|
||||
|
||||
step = tx.Exec(`
|
||||
INSERT INTO transcode_preferences (user_id, client, profile)
|
||||
SELECT user_id, client, profile
|
||||
FROM transcode_preferences_orig;
|
||||
DROP TABLE transcode_preferences_orig;
|
||||
`)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step copy: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateAlbumCreatedAt() gormigrate.Migration {
|
||||
return gormigrate.Migration{
|
||||
ID: "202110041330",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
step := tx.AutoMigrate(
|
||||
Album{},
|
||||
)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step auto migrate: %w", err)
|
||||
}
|
||||
step = tx.Exec(`
|
||||
UPDATE albums SET created_at=modified_at;
|
||||
`)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step migrate album created_at: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func migrateAddAlbumIDX(tx *gorm.DB, _ MigrationContext) error {
|
||||
return tx.AutoMigrate(
|
||||
Album{},
|
||||
).
|
||||
Error
|
||||
}
|
||||
|
||||
func migrateMultiGenre(tx *gorm.DB, _ MigrationContext) error {
|
||||
step := tx.AutoMigrate(
|
||||
Genre{},
|
||||
TrackGenre{},
|
||||
AlbumGenre{},
|
||||
Track{},
|
||||
Album{},
|
||||
)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step auto migrate: %w", err)
|
||||
}
|
||||
|
||||
var genreCount int
|
||||
tx.
|
||||
Model(Genre{}).
|
||||
Count(&genreCount)
|
||||
if genreCount == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
step = tx.Exec(`
|
||||
INSERT INTO track_genres (track_id, genre_id)
|
||||
SELECT id, tag_genre_id
|
||||
FROM tracks
|
||||
WHERE tag_genre_id IS NOT NULL;
|
||||
UPDATE tracks SET tag_genre_id=NULL;
|
||||
`)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step migrate track genres: %w", err)
|
||||
}
|
||||
|
||||
step = tx.Exec(`
|
||||
INSERT INTO album_genres (album_id, genre_id)
|
||||
SELECT id, tag_genre_id
|
||||
FROM albums
|
||||
WHERE tag_genre_id IS NOT NULL;
|
||||
UPDATE albums SET tag_genre_id=NULL;
|
||||
`)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step migrate album genres: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateListenBrainz(tx *gorm.DB, _ MigrationContext) error {
|
||||
step := tx.AutoMigrate(
|
||||
User{},
|
||||
)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step auto migrate: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func migratePodcast(tx *gorm.DB, _ MigrationContext) error {
|
||||
return tx.AutoMigrate(
|
||||
Podcast{},
|
||||
PodcastEpisode{},
|
||||
).
|
||||
Error
|
||||
}
|
||||
|
||||
func migrateBookmarks(tx *gorm.DB, _ MigrationContext) error {
|
||||
return tx.AutoMigrate(
|
||||
Bookmark{},
|
||||
).
|
||||
Error
|
||||
}
|
||||
|
||||
func migratePodcastAutoDownload(tx *gorm.DB, _ MigrationContext) error {
|
||||
return tx.AutoMigrate(
|
||||
Podcast{},
|
||||
).
|
||||
Error
|
||||
}
|
||||
|
||||
func migrateAlbumCreatedAt(tx *gorm.DB, _ MigrationContext) error {
|
||||
step := tx.AutoMigrate(
|
||||
Album{},
|
||||
)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step auto migrate: %w", err)
|
||||
}
|
||||
step = tx.Exec(`
|
||||
UPDATE albums SET created_at=modified_at;
|
||||
`)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step migrate album created_at: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateAlbumRootDir(tx *gorm.DB, ctx MigrationContext) error {
|
||||
var hasIDX int
|
||||
tx.
|
||||
Select("1").
|
||||
Table("sqlite_master").
|
||||
Where("type = ?", "index").
|
||||
Where("name = ?", "idx_album_abs_path").
|
||||
Count(&hasIDX)
|
||||
if hasIDX == 1 {
|
||||
// index already exists
|
||||
return nil
|
||||
}
|
||||
|
||||
step := tx.AutoMigrate(
|
||||
Album{},
|
||||
)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step auto migrate: %w", err)
|
||||
}
|
||||
step = tx.Exec(`
|
||||
DROP INDEX IF EXISTS idx_left_path_right_path;
|
||||
`)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step drop idx: %w", err)
|
||||
}
|
||||
|
||||
step = tx.Exec(`
|
||||
UPDATE albums SET root_dir=?
|
||||
`, ctx.OriginalMusicPath)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step drop idx: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+15
-2
@@ -132,6 +132,18 @@ func (t *Track) MIME() string {
|
||||
return v
|
||||
}
|
||||
|
||||
func (t *Track) AbsPath() string {
|
||||
if t.Album == nil {
|
||||
return ""
|
||||
}
|
||||
return path.Join(
|
||||
t.Album.RootDir,
|
||||
t.Album.LeftPath,
|
||||
t.Album.RightPath,
|
||||
t.Filename,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *Track) RelPath() string {
|
||||
if t.Album == nil {
|
||||
return ""
|
||||
@@ -182,11 +194,12 @@ type Album struct {
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ModifiedAt time.Time
|
||||
LeftPath string `gorm:"unique_index:idx_left_path_right_path"`
|
||||
RightPath string `gorm:"not null; unique_index:idx_left_path_right_path" sql:"default: null"`
|
||||
LeftPath string `gorm:"unique_index:idx_album_abs_path"`
|
||||
RightPath string `gorm:"not null; unique_index:idx_album_abs_path" sql:"default: null"`
|
||||
RightPathUDec string `sql:"default: null"`
|
||||
Parent *Album
|
||||
ParentID int `sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
|
||||
RootDir string `gorm:"unique_index:idx_album_abs_path" sql:"default: null"`
|
||||
Genres []*Genre `gorm:"many2many:album_genres"`
|
||||
Cover string `sql:"default: null"`
|
||||
TagArtist *Artist
|
||||
|
||||
Reference in New Issue
Block a user