refactor: move shared packages up a level
This commit is contained in:
139
db/db.go
Normal file
139
db/db.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
func DefaultOptions() url.Values {
|
||||
return url.Values{
|
||||
// with this, multiple connections share a single data and schema cache.
|
||||
// see https://www.sqlite.org/sharedcache.html
|
||||
"cache": {"shared"},
|
||||
// with this, the db sleeps for a little while when locked. can prevent
|
||||
// a SQLITE_BUSY. see https://www.sqlite.org/c3ref/busy_timeout.html
|
||||
"_busy_timeout": {"30000"},
|
||||
"_journal_mode": {"WAL"},
|
||||
"_foreign_keys": {"true"},
|
||||
}
|
||||
}
|
||||
|
||||
func mockOptions() url.Values {
|
||||
return url.Values{
|
||||
"_foreign_keys": {"true"},
|
||||
}
|
||||
}
|
||||
|
||||
type DB struct {
|
||||
*gorm.DB
|
||||
}
|
||||
|
||||
func New(path string, options url.Values) (*DB, error) {
|
||||
// https://github.com/mattn/go-sqlite3#connection-string
|
||||
url := url.URL{
|
||||
Scheme: "file",
|
||||
Opaque: path,
|
||||
}
|
||||
url.RawQuery = options.Encode()
|
||||
db, err := gorm.Open("sqlite3", url.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("with gorm: %w", err)
|
||||
}
|
||||
db.SetLogger(log.New(os.Stdout, "gorm ", 0))
|
||||
db.DB().SetMaxOpenConns(1)
|
||||
return &DB{DB: db}, nil
|
||||
}
|
||||
|
||||
func NewMock() (*DB, error) {
|
||||
return New(":memory:", mockOptions())
|
||||
}
|
||||
|
||||
func (db *DB) GetSetting(key string) (string, error) {
|
||||
setting := &Setting{}
|
||||
if err := db.Where("key=?", key).First(setting).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", err
|
||||
}
|
||||
return setting.Value, nil
|
||||
}
|
||||
|
||||
func (db *DB) SetSetting(key, value string) error {
|
||||
return db.
|
||||
Where(Setting{Key: key}).
|
||||
Assign(Setting{Value: value}).
|
||||
FirstOrCreate(&Setting{}).
|
||||
Error
|
||||
}
|
||||
|
||||
func (db *DB) InsertBulkLeftMany(table string, head []string, left int, col []int) error {
|
||||
if len(col) == 0 {
|
||||
return nil
|
||||
}
|
||||
var rows []string
|
||||
var values []interface{}
|
||||
for _, c := range col {
|
||||
rows = append(rows, "(?, ?)")
|
||||
values = append(values, left, c)
|
||||
}
|
||||
q := fmt.Sprintf("INSERT OR IGNORE INTO %q (%s) VALUES %s",
|
||||
table,
|
||||
strings.Join(head, ", "),
|
||||
strings.Join(rows, ", "),
|
||||
)
|
||||
return db.Exec(q, values...).Error
|
||||
}
|
||||
|
||||
func (db *DB) GetUserByID(id int) *User {
|
||||
user := &User{}
|
||||
err := db.
|
||||
Where("id=?", id).
|
||||
First(user).
|
||||
Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func (db *DB) GetUserByName(name string) *User {
|
||||
user := &User{}
|
||||
err := db.
|
||||
Where("name=?", name).
|
||||
First(user).
|
||||
Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func (db *DB) Begin() *DB {
|
||||
return &DB{DB: db.DB.Begin()}
|
||||
}
|
||||
|
||||
type ChunkFunc func(*gorm.DB, []int64) error
|
||||
|
||||
func (db *DB) TransactionChunked(data []int64, cb ChunkFunc) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
// https://sqlite.org/limits.html
|
||||
const size = 999
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
for i := 0; i < len(data); i += size {
|
||||
end := i + size
|
||||
if end > len(data) {
|
||||
end = len(data)
|
||||
}
|
||||
if err := cb(tx, data[i:end]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
52
db/db_test.go
Normal file
52
db/db_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
"github.com/matryer/is"
|
||||
)
|
||||
|
||||
func randKey() string {
|
||||
letters := []rune("abcdef0123456789")
|
||||
b := make([]rune, 16)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func TestGetSetting(t *testing.T) {
|
||||
key := randKey()
|
||||
value := "howdy"
|
||||
|
||||
is := is.New(t)
|
||||
|
||||
testDB, err := NewMock()
|
||||
if err != nil {
|
||||
t.Fatalf("error creating db: %v", err)
|
||||
}
|
||||
if err := testDB.Migrate(MigrationContext{}); err != nil {
|
||||
t.Fatalf("error migrating db: %v", err)
|
||||
}
|
||||
|
||||
is.NoErr(testDB.SetSetting(key, value))
|
||||
|
||||
actual, err := testDB.GetSetting(key)
|
||||
is.NoErr(err)
|
||||
is.Equal(actual, value)
|
||||
|
||||
is.NoErr(testDB.SetSetting(key, value))
|
||||
actual, err = testDB.GetSetting(key)
|
||||
is.NoErr(err)
|
||||
is.Equal(actual, value)
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
log.SetOutput(io.Discard)
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
334
db/migrations.go
Normal file
334
db/migrations.go
Normal file
@@ -0,0 +1,334 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"gopkg.in/gormigrate.v1"
|
||||
)
|
||||
|
||||
type MigrationContext struct {
|
||||
OriginalMusicPath string
|
||||
}
|
||||
|
||||
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),
|
||||
construct(ctx, "202201042236", migrateArtistGuessedFolder),
|
||||
construct(ctx, "202202092013", migrateArtistCover),
|
||||
construct(ctx, "202202121809", migrateAlbumRootDirAgain),
|
||||
construct(ctx, "202202241218", migratePublicPlaylist),
|
||||
}
|
||||
|
||||
return gormigrate.
|
||||
New(db.DB, options, migrations).
|
||||
Migrate()
|
||||
}
|
||||
|
||||
func construct(ctx MigrationContext, id string, f func(*gorm.DB, MigrationContext) error) *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: id,
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
tx := db.Begin()
|
||||
defer tx.Commit()
|
||||
if err := f(tx, ctx); err != nil {
|
||||
return fmt.Errorf("%q: %w", id, err)
|
||||
}
|
||||
log.Printf("migration '%s' finished", id)
|
||||
return nil
|
||||
},
|
||||
Rollback: func(*gorm.DB) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateInitSchema(tx *gorm.DB, _ MigrationContext) error {
|
||||
return tx.AutoMigrate(
|
||||
Genre{},
|
||||
TrackGenre{},
|
||||
AlbumGenre{},
|
||||
Track{},
|
||||
Artist{},
|
||||
User{},
|
||||
Setting{},
|
||||
Play{},
|
||||
Album{},
|
||||
Playlist{},
|
||||
PlayQueue{},
|
||||
).
|
||||
Error
|
||||
}
|
||||
|
||||
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 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 migrateCreateTranscode(tx *gorm.DB, _ MigrationContext) error {
|
||||
return tx.AutoMigrate(
|
||||
TranscodePreference{},
|
||||
).
|
||||
Error
|
||||
}
|
||||
|
||||
func migrateAddGenre(tx *gorm.DB, _ MigrationContext) error {
|
||||
return tx.AutoMigrate(
|
||||
Genre{},
|
||||
Album{},
|
||||
Track{},
|
||||
).
|
||||
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 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 {
|
||||
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=? WHERE root_dir IS NULL
|
||||
`, ctx.OriginalMusicPath)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step drop idx: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateArtistGuessedFolder(tx *gorm.DB, ctx MigrationContext) error {
|
||||
return tx.AutoMigrate(Artist{}).Error
|
||||
}
|
||||
|
||||
func migrateArtistCover(tx *gorm.DB, ctx MigrationContext) error {
|
||||
step := tx.AutoMigrate(
|
||||
Artist{},
|
||||
)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step auto migrate: %w", err)
|
||||
}
|
||||
|
||||
if !tx.Dialect().HasColumn("artists", "guessed_folder_id") {
|
||||
return nil
|
||||
}
|
||||
|
||||
step = tx.Exec(`
|
||||
ALTER TABLE artists DROP COLUMN guessed_folder_id
|
||||
`)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step drop column: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// there was an issue with that migration, try it again since it's updated
|
||||
func migrateAlbumRootDirAgain(tx *gorm.DB, ctx MigrationContext) error {
|
||||
return migrateAlbumRootDir(tx, ctx)
|
||||
}
|
||||
|
||||
func migratePublicPlaylist(tx *gorm.DB, ctx MigrationContext) error {
|
||||
return tx.AutoMigrate(Playlist{}).Error
|
||||
}
|
||||
401
db/model.go
Normal file
401
db/model.go
Normal file
@@ -0,0 +1,401 @@
|
||||
// Package db provides database helpers and models
|
||||
//nolint:lll // struct tags get very long and can't be split
|
||||
package db
|
||||
|
||||
// see this db fiddle to mess around with the schema
|
||||
// https://www.db-fiddle.com/f/wJ7z8L7mu6ZKaYmWk1xr1p/5
|
||||
|
||||
import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
// TODO: remove this dep
|
||||
|
||||
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
|
||||
"go.senan.xyz/gonic/mime"
|
||||
)
|
||||
|
||||
func splitInt(in, sep string) []int {
|
||||
if in == "" {
|
||||
return []int{}
|
||||
}
|
||||
parts := strings.Split(in, sep)
|
||||
ret := make([]int, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
i, _ := strconv.Atoi(p)
|
||||
ret = append(ret, i)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func joinInt(in []int, sep string) string {
|
||||
if in == nil {
|
||||
return ""
|
||||
}
|
||||
strs := make([]string, 0, len(in))
|
||||
for _, i := range in {
|
||||
strs = append(strs, strconv.Itoa(i))
|
||||
}
|
||||
return strings.Join(strs, sep)
|
||||
}
|
||||
|
||||
type Artist struct {
|
||||
ID int `gorm:"primary_key"`
|
||||
Name string `gorm:"not null; unique_index"`
|
||||
NameUDec string `sql:"default: null"`
|
||||
Albums []*Album `gorm:"foreignkey:TagArtistID"`
|
||||
AlbumCount int `sql:"-"`
|
||||
Cover string `sql:"default: null"`
|
||||
}
|
||||
|
||||
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"`
|
||||
Artist *Artist
|
||||
ArtistID int `gorm:"not null" sql:"default: null; type:int REFERENCES artists(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"`
|
||||
}
|
||||
|
||||
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) ArtistSID() *specid.ID {
|
||||
return &specid.ID{Type: specid.Artist, Value: t.ArtistID}
|
||||
}
|
||||
|
||||
func (t *Track) Ext() string {
|
||||
longExt := path.Ext(t.Filename)
|
||||
if len(longExt) < 1 {
|
||||
return ""
|
||||
}
|
||||
return longExt[1:]
|
||||
}
|
||||
|
||||
func (t *Track) AudioFilename() string {
|
||||
return t.Filename
|
||||
}
|
||||
|
||||
func (t *Track) MIME() string {
|
||||
v, _ := mime.FromExtension(t.Ext())
|
||||
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 ""
|
||||
}
|
||||
return path.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"`
|
||||
}
|
||||
|
||||
type Setting struct {
|
||||
Key string `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
|
||||
}
|
||||
|
||||
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"`
|
||||
TagArtist *Artist
|
||||
TagArtistID int `gorm:"index" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
|
||||
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:"-"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type Playlist 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"`
|
||||
Name string
|
||||
Comment string
|
||||
TrackCount int
|
||||
Items string
|
||||
IsPublic bool `sql:"default: null"`
|
||||
}
|
||||
|
||||
func (p *Playlist) GetItems() []int {
|
||||
return splitInt(p.Items, ",")
|
||||
}
|
||||
|
||||
func (p *Playlist) SetItems(items []int) {
|
||||
p.Items = joinInt(items, ",")
|
||||
p.TrackCount = len(items)
|
||||
}
|
||||
|
||||
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 int
|
||||
Position int
|
||||
ChangedBy string
|
||||
Items string
|
||||
}
|
||||
|
||||
func (p *PlayQueue) CurrentSID() *specid.ID {
|
||||
return &specid.ID{Type: specid.Track, Value: p.Current}
|
||||
}
|
||||
|
||||
func (p *PlayQueue) GetItems() []int {
|
||||
return splitInt(p.Items, ",")
|
||||
}
|
||||
|
||||
func (p *PlayQueue) SetItems(items []int) {
|
||||
p.Items = joinInt(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 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 PodcastAutoDownload string
|
||||
|
||||
const (
|
||||
PodcastAutoDownloadLatest PodcastAutoDownload = "latest"
|
||||
PodcastAutoDownloadNone PodcastAutoDownload = "none"
|
||||
)
|
||||
|
||||
type Podcast struct {
|
||||
ID int `gorm:"primary_key"`
|
||||
UpdatedAt time.Time
|
||||
ModifiedAt time.Time
|
||||
UserID int `sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"`
|
||||
URL string
|
||||
Title string
|
||||
Description string
|
||||
ImageURL string
|
||||
ImagePath string
|
||||
Error string
|
||||
Episodes []*PodcastEpisode
|
||||
AutoDownload PodcastAutoDownload
|
||||
}
|
||||
|
||||
func (p *Podcast) Fullpath(podcastPath string) string {
|
||||
sanitizedTitle := strings.ReplaceAll(p.Title, "/", "_")
|
||||
return filepath.Join(podcastPath, filepath.Clean(sanitizedTitle))
|
||||
}
|
||||
|
||||
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
|
||||
Path string
|
||||
Filename string
|
||||
Status PodcastEpisodeStatus
|
||||
Error string
|
||||
}
|
||||
|
||||
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) AudioFilename() string {
|
||||
return pe.Filename
|
||||
}
|
||||
|
||||
func (pe *PodcastEpisode) Ext() string {
|
||||
longExt := path.Ext(pe.Filename)
|
||||
if len(longExt) < 1 {
|
||||
return ""
|
||||
}
|
||||
return longExt[1:]
|
||||
}
|
||||
|
||||
func (pe *PodcastEpisode) MIME() string {
|
||||
v, _ := mime.FromExtension(pe.Ext())
|
||||
return v
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user