move mockfs DumpDB to db package
This commit is contained in:
496
db/db.go
496
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
459
db/model.go
459
db/model.go
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user