From 66a29f7e93ada228f029603361cf37456ad5a361 Mon Sep 17 00:00:00 2001 From: sentriz Date: Mon, 9 Oct 2023 21:50:34 +0100 Subject: [PATCH] move mockfs DumpDB to db package --- db/db.go | 496 ++++++++++++++++++++++++++++++++++++ db/migrations.go | 4 +- db/migrations_old_models.go | 4 +- db/model.go | 459 --------------------------------- mockfs/mockfs.go | 40 +-- 5 files changed, 502 insertions(+), 501 deletions(-) delete mode 100644 db/model.go diff --git a/db/db.go b/db/db.go index 4f6240e..bca5d61 100644 --- a/db/db.go +++ b/db/db.go @@ -1,14 +1,23 @@ package db import ( + "context" "errors" "fmt" "log" + "mime" "net/url" "os" + "path/filepath" + "sort" "strings" + "time" "github.com/jinzhu/gorm" + "github.com/mattn/go-sqlite3" + + // TODO: remove this dep + "go.senan.xyz/gonic/server/ctrlsubsonic/specid" ) func DefaultOptions() url.Values { @@ -145,3 +154,490 @@ func (db *DB) SetSetting(key SettingKey, value string) error { FirstOrCreate(&Setting{}). Error } + +type Artist struct { + ID int `gorm:"primary_key"` + Name string `gorm:"not null; unique_index"` + NameUDec string `sql:"default: null"` + Albums []*Album `gorm:"many2many:album_artists"` + AlbumCount int `sql:"-"` + ArtistStar *ArtistStar + ArtistRating *ArtistRating + AverageRating float64 `sql:"default: null"` + Info *ArtistInfo `gorm:"foreignkey:id"` +} + +func (a *Artist) SID() *specid.ID { + return &specid.ID{Type: specid.Artist, Value: a.ID} +} + +func (a *Artist) IndexName() string { + if len(a.NameUDec) > 0 { + return a.NameUDec + } + return a.Name +} + +type Genre struct { + ID int `gorm:"primary_key"` + Name string `gorm:"not null; unique_index"` + AlbumCount int `sql:"-"` + TrackCount int `sql:"-"` +} + +// AudioFile is used to avoid some duplication in handlers_raw.go +// between Track and Podcast +type AudioFile interface { + Ext() string + MIME() string + AudioFilename() string + AudioBitrate() int + AudioLength() int +} + +type Track struct { + ID int `gorm:"primary_key"` + CreatedAt time.Time + UpdatedAt time.Time + Filename string `gorm:"not null; unique_index:idx_folder_filename" sql:"default: null"` + FilenameUDec string `sql:"default: null"` + Album *Album + AlbumID int `gorm:"not null; unique_index:idx_folder_filename" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` + Genres []*Genre `gorm:"many2many:track_genres"` + Size int `sql:"default: null"` + Length int `sql:"default: null"` + Bitrate int `sql:"default: null"` + TagTitle string `sql:"default: null"` + TagTitleUDec string `sql:"default: null"` + TagTrackArtist string `sql:"default: null"` + TagTrackNumber int `sql:"default: null"` + TagDiscNumber int `sql:"default: null"` + TagBrainzID string `sql:"default: null"` + TrackStar *TrackStar + TrackRating *TrackRating + AverageRating float64 `sql:"default: null"` +} + +func (t *Track) AudioLength() int { return t.Length } +func (t *Track) AudioBitrate() int { return t.Bitrate } + +func (t *Track) SID() *specid.ID { + return &specid.ID{Type: specid.Track, Value: t.ID} +} + +func (t *Track) AlbumSID() *specid.ID { + return &specid.ID{Type: specid.Album, Value: t.AlbumID} +} + +func (t *Track) Ext() string { + return filepath.Ext(t.Filename) +} + +func (t *Track) AudioFilename() string { + return t.Filename +} + +func (t *Track) MIME() string { + return mime.TypeByExtension(filepath.Ext(t.Filename)) +} + +func (t *Track) AbsPath() string { + if t.Album == nil { + return "" + } + return filepath.Join( + t.Album.RootDir, + t.Album.LeftPath, + t.Album.RightPath, + t.Filename, + ) +} + +func (t *Track) RelPath() string { + if t.Album == nil { + return "" + } + return filepath.Join( + t.Album.LeftPath, + t.Album.RightPath, + t.Filename, + ) +} + +func (t *Track) GenreStrings() []string { + strs := make([]string, 0, len(t.Genres)) + for _, genre := range t.Genres { + strs = append(strs, genre.Name) + } + return strs +} + +type User struct { + ID int `gorm:"primary_key"` + CreatedAt time.Time + Name string `gorm:"not null; unique_index" sql:"default: null"` + Password string `gorm:"not null" sql:"default: null"` + LastFMSession string `sql:"default: null"` + ListenBrainzURL string `sql:"default: null"` + ListenBrainzToken string `sql:"default: null"` + IsAdmin bool `sql:"default: null"` + Avatar []byte `sql:"default: null"` +} + +type Setting struct { + Key SettingKey `gorm:"not null; primary_key; auto_increment:false" sql:"default: null"` + Value string `sql:"default: null"` +} + +type Play struct { + ID int `gorm:"primary_key"` + User *User + UserID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` + Album *Album + AlbumID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` + Time time.Time `sql:"default: null"` + Count int + Length int +} + +type Album struct { + ID int `gorm:"primary_key"` + CreatedAt time.Time + UpdatedAt time.Time + ModifiedAt time.Time + 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"` + Artists []*Artist `gorm:"many2many:album_artists"` + TagTitle string `sql:"default: null"` + TagTitleUDec string `sql:"default: null"` + TagBrainzID string `sql:"default: null"` + TagYear int `sql:"default: null"` + Tracks []*Track + ChildCount int `sql:"-"` + Duration int `sql:"-"` + AlbumStar *AlbumStar + AlbumRating *AlbumRating + AverageRating float64 `sql:"default: null"` +} + +func (a *Album) SID() *specid.ID { + return &specid.ID{Type: specid.Album, Value: a.ID} +} + +func (a *Album) ParentSID() *specid.ID { + return &specid.ID{Type: specid.Album, Value: a.ParentID} +} + +func (a *Album) IndexRightPath() string { + if len(a.RightPathUDec) > 0 { + return a.RightPathUDec + } + return a.RightPath +} + +func (a *Album) GenreStrings() []string { + strs := make([]string, 0, len(a.Genres)) + for _, genre := range a.Genres { + strs = append(strs, genre.Name) + } + return strs +} + +func (a *Album) ArtistsStrings() []string { + artists := append([]*Artist(nil), a.Artists...) + sort.Slice(artists, func(i, j int) bool { + return artists[i].ID < artists[j].ID + }) + strs := make([]string, 0, len(artists)) + for _, artist := range artists { + strs = append(strs, artist.Name) + } + return strs +} + +type PlayQueue struct { + ID int `gorm:"primary_key"` + CreatedAt time.Time + UpdatedAt time.Time + User *User + UserID int `sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` + Current string + Position int + ChangedBy string + Items string +} + +func (p *PlayQueue) CurrentSID() *specid.ID { + id, _ := specid.New(p.Current) + return &id +} + +func (p *PlayQueue) GetItems() []specid.ID { + return splitIDs(p.Items, ",") +} + +func (p *PlayQueue) SetItems(items []specid.ID) { + p.Items = join(items, ",") +} + +type TranscodePreference struct { + User *User + UserID int `gorm:"not null; unique_index:idx_user_id_client" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` + Client string `gorm:"not null; unique_index:idx_user_id_client" sql:"default: null"` + Profile string `gorm:"not null" sql:"default: null"` +} + +type AlbumArtist struct { + Album *Album + AlbumID int `gorm:"not null; unique_index:idx_album_id_artist_id" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` + Artist *Artist + ArtistID int `gorm:"not null; unique_index:idx_album_id_artist_id" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"` +} + +type TrackGenre struct { + Track *Track + TrackID int `gorm:"not null; unique_index:idx_track_id_genre_id" sql:"default: null; type:int REFERENCES tracks(id) ON DELETE CASCADE"` + Genre *Genre + GenreID int `gorm:"not null; unique_index:idx_track_id_genre_id" sql:"default: null; type:int REFERENCES genres(id) ON DELETE CASCADE"` +} + +type AlbumGenre struct { + Album *Album + AlbumID int `gorm:"not null; unique_index:idx_album_id_genre_id" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` + Genre *Genre + GenreID int `gorm:"not null; unique_index:idx_album_id_genre_id" sql:"default: null; type:int REFERENCES genres(id) ON DELETE CASCADE"` +} + +type AlbumStar struct { + UserID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` + AlbumID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` + StarDate time.Time +} + +type AlbumRating struct { + UserID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` + AlbumID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` + Rating int `gorm:"not null; check:(rating >= 1 AND rating <= 5)"` +} + +type ArtistStar struct { + UserID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` + ArtistID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"` + StarDate time.Time +} + +type ArtistRating struct { + UserID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` + ArtistID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"` + Rating int `gorm:"not null; check:(rating >= 1 AND rating <= 5)"` +} + +type TrackStar struct { + UserID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` + TrackID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES tracks(id) ON DELETE CASCADE"` + StarDate time.Time +} + +type TrackRating struct { + UserID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` + TrackID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES tracks(id) ON DELETE CASCADE"` + Rating int `gorm:"not null; check:(rating >= 1 AND rating <= 5)"` +} + +type PodcastAutoDownload string + +const ( + PodcastAutoDownloadLatest PodcastAutoDownload = "latest" + PodcastAutoDownloadNone PodcastAutoDownload = "none" +) + +type Podcast struct { + ID int `gorm:"primary_key"` + UpdatedAt time.Time + ModifiedAt time.Time + URL string + Title string + Description string + ImageURL string + Image string + Error string + Episodes []*PodcastEpisode + AutoDownload PodcastAutoDownload + RootDir string +} + +func (p *Podcast) SID() *specid.ID { + return &specid.ID{Type: specid.Podcast, Value: p.ID} +} + +type PodcastEpisodeStatus string + +const ( + PodcastEpisodeStatusDownloading PodcastEpisodeStatus = "downloading" + PodcastEpisodeStatusSkipped PodcastEpisodeStatus = "skipped" + PodcastEpisodeStatusDeleted PodcastEpisodeStatus = "deleted" + PodcastEpisodeStatusCompleted PodcastEpisodeStatus = "completed" + PodcastEpisodeStatusError PodcastEpisodeStatus = "error" +) + +type PodcastEpisode struct { + ID int `gorm:"primary_key"` + CreatedAt time.Time + UpdatedAt time.Time + ModifiedAt time.Time + PodcastID int `gorm:"not null" sql:"default: null; type:int REFERENCES podcasts(id) ON DELETE CASCADE"` + Title string + Description string + PublishDate *time.Time + AudioURL string + Bitrate int + Length int + Size int + Filename string + Status PodcastEpisodeStatus + Error string + Podcast *Podcast +} + +func (pe *PodcastEpisode) AudioLength() int { return pe.Length } +func (pe *PodcastEpisode) AudioBitrate() int { return pe.Bitrate } + +func (pe *PodcastEpisode) SID() *specid.ID { + return &specid.ID{Type: specid.PodcastEpisode, Value: pe.ID} +} + +func (pe *PodcastEpisode) PodcastSID() *specid.ID { + return &specid.ID{Type: specid.Podcast, Value: pe.PodcastID} +} + +func (pe *PodcastEpisode) AudioFilename() string { + return pe.Filename +} + +func (pe *PodcastEpisode) Ext() string { + return filepath.Ext(pe.Filename) +} + +func (pe *PodcastEpisode) MIME() string { + return mime.TypeByExtension(filepath.Ext(pe.Filename)) +} + +func (pe *PodcastEpisode) AbsPath() string { + if pe.Podcast == nil { + return "" + } + return filepath.Join(pe.Podcast.RootDir, pe.Filename) +} + +type Bookmark struct { + ID int `gorm:"primary_key"` + User *User + UserID int `sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` + Position int + Comment string + EntryIDType string + EntryID int + CreatedAt time.Time + UpdatedAt time.Time +} + +type InternetRadioStation struct { + ID int `gorm:"primary_key"` + StreamURL string + Name string + HomepageURL string +} + +func (ir *InternetRadioStation) SID() *specid.ID { + return &specid.ID{Type: specid.InternetRadioStation, Value: ir.ID} +} + +type ArtistInfo struct { + ID int `gorm:"primary_key" sql:"type:int REFERENCES artists(id) ON DELETE CASCADE"` + CreatedAt time.Time + UpdatedAt time.Time `gorm:"index"` + Biography string + MusicBrainzID string + LastFMURL string + ImageURL string + SimilarArtists string + TopTracks string +} + +func (p *ArtistInfo) GetSimilarArtists() []string { return strings.Split(p.SimilarArtists, ";") } +func (p *ArtistInfo) SetSimilarArtists(items []string) { p.SimilarArtists = strings.Join(items, ";") } + +func (p *ArtistInfo) GetTopTracks() []string { return strings.Split(p.TopTracks, ";") } +func (p *ArtistInfo) SetTopTracks(items []string) { p.TopTracks = strings.Join(items, ";") } + +func splitIDs(in, sep string) []specid.ID { + if in == "" { + return []specid.ID{} + } + parts := strings.Split(in, sep) + ret := make([]specid.ID, 0, len(parts)) + for _, p := range parts { + id, _ := specid.New(p) + ret = append(ret, id) + } + return ret +} + +func join[T fmt.Stringer](in []T, sep string) string { + if in == nil { + return "" + } + strs := make([]string, 0, len(in)) + for _, id := range in { + strs = append(strs, id.String()) + } + return strings.Join(strs, sep) +} + +func Dump(ctx context.Context, db *gorm.DB, to string) error { + dest, err := New(to, url.Values{}) + if err != nil { + return fmt.Errorf("create dest db: %w", err) + } + defer dest.Close() + + connSrc, err := db.DB().Conn(ctx) + if err != nil { + return fmt.Errorf("getting src raw conn: %w", err) + } + defer connSrc.Close() + + connDest, err := dest.DB.DB().Conn(ctx) + if err != nil { + return fmt.Errorf("getting dest raw conn: %w", err) + } + defer connDest.Close() + + err = connDest.Raw(func(connDest interface{}) error { + return connSrc.Raw(func(connSrc interface{}) error { + connDestq := connDest.(*sqlite3.SQLiteConn) + connSrcq := connSrc.(*sqlite3.SQLiteConn) + bk, err := connDestq.Backup("main", connSrcq, "main") + if err != nil { + return fmt.Errorf("create backup db: %w", err) + } + for done, _ := bk.Step(-1); !done; { //nolint: revive + } + if err := bk.Finish(); err != nil { + return fmt.Errorf("finishing dump: %w", err) + } + return nil + }) + }) + if err != nil { + return fmt.Errorf("backing up: %w", err) + } + + return nil +} diff --git a/db/migrations.go b/db/migrations.go index 9739b7c..314a595 100644 --- a/db/migrations.go +++ b/db/migrations.go @@ -356,7 +356,7 @@ func migratePublicPlaylist(tx *gorm.DB, _ MigrationContext) error { if !tx.HasTable("playlists") { return nil } - return tx.AutoMigrate(_OldPlaylist{}).Error + return tx.AutoMigrate(__OldPlaylist{}).Error } func migratePodcastDropUserID(tx *gorm.DB, _ MigrationContext) error { @@ -495,7 +495,7 @@ func migratePlaylistsToM3U(tx *gorm.DB, ctx MigrationContext) error { return fmt.Errorf("create playlists store: %w", err) } - var prevs []*_OldPlaylist + var prevs []*__OldPlaylist if err := tx.Find(&prevs).Error; err != nil { return fmt.Errorf("fetch old playlists: %w", err) } diff --git a/db/migrations_old_models.go b/db/migrations_old_models.go index ea727aa..7df9f45 100644 --- a/db/migrations_old_models.go +++ b/db/migrations_old_models.go @@ -2,7 +2,7 @@ package db import "time" -type _OldPlaylist struct { +type __OldPlaylist struct { //nolint: revive,stylecheck ID int `gorm:"primary_key"` CreatedAt time.Time UpdatedAt time.Time @@ -15,6 +15,6 @@ type _OldPlaylist struct { IsPublic bool `sql:"default: null"` } -func (_OldPlaylist) TableName() string { +func (__OldPlaylist) TableName() string { return "playlists" } diff --git a/db/model.go b/db/model.go deleted file mode 100644 index 20a8885..0000000 --- a/db/model.go +++ /dev/null @@ -1,459 +0,0 @@ -//nolint:lll // struct tags get very long and can't be split -package db - -import ( - "fmt" - "mime" - "path/filepath" - "sort" - "strings" - "time" - - // TODO: remove this dep - "go.senan.xyz/gonic/server/ctrlsubsonic/specid" -) - -type Artist struct { - ID int `gorm:"primary_key"` - Name string `gorm:"not null; unique_index"` - NameUDec string `sql:"default: null"` - Albums []*Album `gorm:"many2many:album_artists"` - AlbumCount int `sql:"-"` - ArtistStar *ArtistStar - ArtistRating *ArtistRating - AverageRating float64 `sql:"default: null"` - Info *ArtistInfo `gorm:"foreignkey:id"` -} - -func (a *Artist) SID() *specid.ID { - return &specid.ID{Type: specid.Artist, Value: a.ID} -} - -func (a *Artist) IndexName() string { - if len(a.NameUDec) > 0 { - return a.NameUDec - } - return a.Name -} - -type Genre struct { - ID int `gorm:"primary_key"` - Name string `gorm:"not null; unique_index"` - AlbumCount int `sql:"-"` - TrackCount int `sql:"-"` -} - -// AudioFile is used to avoid some duplication in handlers_raw.go -// between Track and Podcast -type AudioFile interface { - Ext() string - MIME() string - AudioFilename() string - AudioBitrate() int - AudioLength() int -} - -type Track struct { - ID int `gorm:"primary_key"` - CreatedAt time.Time - UpdatedAt time.Time - Filename string `gorm:"not null; unique_index:idx_folder_filename" sql:"default: null"` - FilenameUDec string `sql:"default: null"` - Album *Album - AlbumID int `gorm:"not null; unique_index:idx_folder_filename" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` - Genres []*Genre `gorm:"many2many:track_genres"` - Size int `sql:"default: null"` - Length int `sql:"default: null"` - Bitrate int `sql:"default: null"` - TagTitle string `sql:"default: null"` - TagTitleUDec string `sql:"default: null"` - TagTrackArtist string `sql:"default: null"` - TagTrackNumber int `sql:"default: null"` - TagDiscNumber int `sql:"default: null"` - TagBrainzID string `sql:"default: null"` - TrackStar *TrackStar - TrackRating *TrackRating - AverageRating float64 `sql:"default: null"` -} - -func (t *Track) AudioLength() int { return t.Length } -func (t *Track) AudioBitrate() int { return t.Bitrate } - -func (t *Track) SID() *specid.ID { - return &specid.ID{Type: specid.Track, Value: t.ID} -} - -func (t *Track) AlbumSID() *specid.ID { - return &specid.ID{Type: specid.Album, Value: t.AlbumID} -} - -func (t *Track) Ext() string { - return filepath.Ext(t.Filename) -} - -func (t *Track) AudioFilename() string { - return t.Filename -} - -func (t *Track) MIME() string { - return mime.TypeByExtension(filepath.Ext(t.Filename)) -} - -func (t *Track) AbsPath() string { - if t.Album == nil { - return "" - } - return filepath.Join( - t.Album.RootDir, - t.Album.LeftPath, - t.Album.RightPath, - t.Filename, - ) -} - -func (t *Track) RelPath() string { - if t.Album == nil { - return "" - } - return filepath.Join( - t.Album.LeftPath, - t.Album.RightPath, - t.Filename, - ) -} - -func (t *Track) GenreStrings() []string { - strs := make([]string, 0, len(t.Genres)) - for _, genre := range t.Genres { - strs = append(strs, genre.Name) - } - return strs -} - -type User struct { - ID int `gorm:"primary_key"` - CreatedAt time.Time - Name string `gorm:"not null; unique_index" sql:"default: null"` - Password string `gorm:"not null" sql:"default: null"` - LastFMSession string `sql:"default: null"` - ListenBrainzURL string `sql:"default: null"` - ListenBrainzToken string `sql:"default: null"` - IsAdmin bool `sql:"default: null"` - Avatar []byte `sql:"default: null"` -} - -type Setting struct { - Key SettingKey `gorm:"not null; primary_key; auto_increment:false" sql:"default: null"` - Value string `sql:"default: null"` -} - -type Play struct { - ID int `gorm:"primary_key"` - User *User - UserID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` - Album *Album - AlbumID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` - Time time.Time `sql:"default: null"` - Count int - Length int -} - -type Album struct { - ID int `gorm:"primary_key"` - CreatedAt time.Time - UpdatedAt time.Time - ModifiedAt time.Time - 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"` - Artists []*Artist `gorm:"many2many:album_artists"` - TagTitle string `sql:"default: null"` - TagTitleUDec string `sql:"default: null"` - TagBrainzID string `sql:"default: null"` - TagYear int `sql:"default: null"` - Tracks []*Track - ChildCount int `sql:"-"` - Duration int `sql:"-"` - AlbumStar *AlbumStar - AlbumRating *AlbumRating - AverageRating float64 `sql:"default: null"` -} - -func (a *Album) SID() *specid.ID { - return &specid.ID{Type: specid.Album, Value: a.ID} -} - -func (a *Album) ParentSID() *specid.ID { - return &specid.ID{Type: specid.Album, Value: a.ParentID} -} - -func (a *Album) IndexRightPath() string { - if len(a.RightPathUDec) > 0 { - return a.RightPathUDec - } - return a.RightPath -} - -func (a *Album) GenreStrings() []string { - strs := make([]string, 0, len(a.Genres)) - for _, genre := range a.Genres { - strs = append(strs, genre.Name) - } - return strs -} - -func (a *Album) ArtistsStrings() []string { - artists := append([]*Artist(nil), a.Artists...) - sort.Slice(artists, func(i, j int) bool { - return artists[i].ID < artists[j].ID - }) - strs := make([]string, 0, len(artists)) - for _, artist := range artists { - strs = append(strs, artist.Name) - } - return strs -} - -type PlayQueue struct { - ID int `gorm:"primary_key"` - CreatedAt time.Time - UpdatedAt time.Time - User *User - UserID int `sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` - Current string - Position int - ChangedBy string - Items string -} - -func (p *PlayQueue) CurrentSID() *specid.ID { - id, _ := specid.New(p.Current) - return &id -} - -func (p *PlayQueue) GetItems() []specid.ID { - return splitIDs(p.Items, ",") -} - -func (p *PlayQueue) SetItems(items []specid.ID) { - p.Items = join(items, ",") -} - -type TranscodePreference struct { - User *User - UserID int `gorm:"not null; unique_index:idx_user_id_client" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` - Client string `gorm:"not null; unique_index:idx_user_id_client" sql:"default: null"` - Profile string `gorm:"not null" sql:"default: null"` -} - -type AlbumArtist struct { - Album *Album - AlbumID int `gorm:"not null; unique_index:idx_album_id_artist_id" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` - Artist *Artist - ArtistID int `gorm:"not null; unique_index:idx_album_id_artist_id" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"` -} - -type TrackGenre struct { - Track *Track - TrackID int `gorm:"not null; unique_index:idx_track_id_genre_id" sql:"default: null; type:int REFERENCES tracks(id) ON DELETE CASCADE"` - Genre *Genre - GenreID int `gorm:"not null; unique_index:idx_track_id_genre_id" sql:"default: null; type:int REFERENCES genres(id) ON DELETE CASCADE"` -} - -type AlbumGenre struct { - Album *Album - AlbumID int `gorm:"not null; unique_index:idx_album_id_genre_id" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` - Genre *Genre - GenreID int `gorm:"not null; unique_index:idx_album_id_genre_id" sql:"default: null; type:int REFERENCES genres(id) ON DELETE CASCADE"` -} - -type AlbumStar struct { - UserID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` - AlbumID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` - StarDate time.Time -} - -type AlbumRating struct { - UserID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` - AlbumID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` - Rating int `gorm:"not null; check:(rating >= 1 AND rating <= 5)"` -} - -type ArtistStar struct { - UserID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` - ArtistID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"` - StarDate time.Time -} - -type ArtistRating struct { - UserID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` - ArtistID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"` - Rating int `gorm:"not null; check:(rating >= 1 AND rating <= 5)"` -} - -type TrackStar struct { - UserID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` - TrackID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES tracks(id) ON DELETE CASCADE"` - StarDate time.Time -} - -type TrackRating struct { - UserID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` - TrackID int `gorm:"primary_key; not null" sql:"default: null; type:int REFERENCES tracks(id) ON DELETE CASCADE"` - Rating int `gorm:"not null; check:(rating >= 1 AND rating <= 5)"` -} - -type PodcastAutoDownload string - -const ( - PodcastAutoDownloadLatest PodcastAutoDownload = "latest" - PodcastAutoDownloadNone PodcastAutoDownload = "none" -) - -type Podcast struct { - ID int `gorm:"primary_key"` - UpdatedAt time.Time - ModifiedAt time.Time - URL string - Title string - Description string - ImageURL string - Image string - Error string - Episodes []*PodcastEpisode - AutoDownload PodcastAutoDownload - RootDir string -} - -func (p *Podcast) SID() *specid.ID { - return &specid.ID{Type: specid.Podcast, Value: p.ID} -} - -type PodcastEpisodeStatus string - -const ( - PodcastEpisodeStatusDownloading PodcastEpisodeStatus = "downloading" - PodcastEpisodeStatusSkipped PodcastEpisodeStatus = "skipped" - PodcastEpisodeStatusDeleted PodcastEpisodeStatus = "deleted" - PodcastEpisodeStatusCompleted PodcastEpisodeStatus = "completed" - PodcastEpisodeStatusError PodcastEpisodeStatus = "error" -) - -type PodcastEpisode struct { - ID int `gorm:"primary_key"` - CreatedAt time.Time - UpdatedAt time.Time - ModifiedAt time.Time - PodcastID int `gorm:"not null" sql:"default: null; type:int REFERENCES podcasts(id) ON DELETE CASCADE"` - Title string - Description string - PublishDate *time.Time - AudioURL string - Bitrate int - Length int - Size int - Filename string - Status PodcastEpisodeStatus - Error string - Podcast *Podcast -} - -func (pe *PodcastEpisode) AudioLength() int { return pe.Length } -func (pe *PodcastEpisode) AudioBitrate() int { return pe.Bitrate } - -func (pe *PodcastEpisode) SID() *specid.ID { - return &specid.ID{Type: specid.PodcastEpisode, Value: pe.ID} -} - -func (pe *PodcastEpisode) PodcastSID() *specid.ID { - return &specid.ID{Type: specid.Podcast, Value: pe.PodcastID} -} - -func (pe *PodcastEpisode) AudioFilename() string { - return pe.Filename -} - -func (pe *PodcastEpisode) Ext() string { - return filepath.Ext(pe.Filename) -} - -func (pe *PodcastEpisode) MIME() string { - return mime.TypeByExtension(filepath.Ext(pe.Filename)) -} - -func (pe *PodcastEpisode) AbsPath() string { - if pe.Podcast == nil { - return "" - } - return filepath.Join(pe.Podcast.RootDir, pe.Filename) -} - -type Bookmark struct { - ID int `gorm:"primary_key"` - User *User - UserID int `sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` - Position int - Comment string - EntryIDType string - EntryID int - CreatedAt time.Time - UpdatedAt time.Time -} - -type InternetRadioStation struct { - ID int `gorm:"primary_key"` - StreamURL string - Name string - HomepageURL string -} - -func (ir *InternetRadioStation) SID() *specid.ID { - return &specid.ID{Type: specid.InternetRadioStation, Value: ir.ID} -} - -type ArtistInfo struct { - ID int `gorm:"primary_key" sql:"type:int REFERENCES artists(id) ON DELETE CASCADE"` - CreatedAt time.Time - UpdatedAt time.Time `gorm:"index"` - Biography string - MusicBrainzID string - LastFMURL string - ImageURL string - SimilarArtists string - TopTracks string -} - -func (p *ArtistInfo) GetSimilarArtists() []string { return strings.Split(p.SimilarArtists, ";") } -func (p *ArtistInfo) SetSimilarArtists(items []string) { p.SimilarArtists = strings.Join(items, ";") } - -func (p *ArtistInfo) GetTopTracks() []string { return strings.Split(p.TopTracks, ";") } -func (p *ArtistInfo) SetTopTracks(items []string) { p.TopTracks = strings.Join(items, ";") } - -func splitIDs(in, sep string) []specid.ID { - if in == "" { - return []specid.ID{} - } - parts := strings.Split(in, sep) - ret := make([]specid.ID, 0, len(parts)) - for _, p := range parts { - id, _ := specid.New(p) - ret = append(ret, id) - } - return ret -} - -func join[T fmt.Stringer](in []T, sep string) string { - if in == nil { - return "" - } - strs := make([]string, 0, len(in)) - for _, id := range in { - strs = append(strs, id.String()) - } - return strings.Join(strs, sep) -} diff --git a/mockfs/mockfs.go b/mockfs/mockfs.go index 8de8238..ea332ee 100644 --- a/mockfs/mockfs.go +++ b/mockfs/mockfs.go @@ -5,14 +5,12 @@ import ( "context" "errors" "fmt" - "net/url" "os" "path/filepath" "strings" "testing" "time" - "github.com/mattn/go-sqlite3" "go.senan.xyz/gonic/db" "go.senan.xyz/gonic/scanner" "go.senan.xyz/gonic/tags/tagcommon" @@ -311,42 +309,8 @@ func (m *MockFS) DumpDB(suffix ...string) { p = append(p, suffix...) destPath := filepath.Join(os.TempDir(), strings.Join(p, "-")) - dest, err := db.New(destPath, url.Values{}) - if err != nil { - m.t.Fatalf("create dest db: %v", err) - } - defer dest.Close() - - connSrc, err := m.db.DB.DB().Conn(context.Background()) - if err != nil { - m.t.Fatalf("getting src raw conn: %v", err) - } - defer connSrc.Close() - connDest, err := dest.DB.DB().Conn(context.Background()) - if err != nil { - m.t.Fatalf("getting dest raw conn: %v", err) - } - defer connDest.Close() - - err = connDest.Raw(func(connDest interface{}) error { - return connSrc.Raw(func(connSrc interface{}) error { - connDestq := connDest.(*sqlite3.SQLiteConn) - connSrcq := connSrc.(*sqlite3.SQLiteConn) - bk, err := connDestq.Backup("main", connSrcq, "main") - if err != nil { - return fmt.Errorf("create backup db: %w", err) - } - for done, _ := bk.Step(-1); !done; { - m.t.Logf("dumping db...") - } - if err := bk.Finish(); err != nil { - return fmt.Errorf("finishing dump: %w", err) - } - return nil - }) - }) - if err != nil { - m.t.Fatalf("backing up: %v", err) + if err := db.Dump(context.Background(), m.db.DB, destPath); err != nil { + m.t.Fatalf("dumping db: %v", err) } m.t.Error(destPath)