diff --git a/db/migrations/20240511220020_add_library_table.go b/db/migrations/20240511220020_add_library_table.go new file mode 100644 index 00000000..ec943b42 --- /dev/null +++ b/db/migrations/20240511220020_add_library_table.go @@ -0,0 +1,71 @@ +package migrations + +import ( + "context" + "database/sql" + "fmt" + + "github.com/navidrome/navidrome/conf" + "github.com/pressly/goose/v3" +) + +func init() { + goose.AddMigrationContext(upAddLibraryTable, downAddLibraryTable) +} + +func upAddLibraryTable(ctx context.Context, tx *sql.Tx) error { + _, err := tx.ExecContext(ctx, ` + create table library ( + id integer primary key autoincrement, + name text not null unique, + path text not null unique, + remote_path text null default '', + last_scan_at datetime not null default '0000-00-00 00:00:00', + updated_at datetime not null default current_timestamp, + created_at datetime not null default current_timestamp + );`) + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, fmt.Sprintf(` + insert into library(id, name, path, last_scan_at) values(1, 'Music Library', '%s', current_timestamp); + delete from property where id like 'LastScan-%%'; +`, conf.Server.MusicFolder)) + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, ` + alter table media_file add column library_id integer not null default 1 + references library(id) on delete cascade; + alter table album add column library_id integer not null default 1 + references library(id) on delete cascade; + + create table if not exists library_artist + ( + library_id integer not null default 1 + references library(id) + on delete cascade, + artist_id varchar not null default null + references artist(id) + on delete cascade, + constraint library_artist_ux + unique (library_id, artist_id) + ); + + insert into library_artist(library_id, artist_id) select 1, id from artist; +`) + + return err +} + +func downAddLibraryTable(ctx context.Context, tx *sql.Tx) error { + _, err := tx.ExecContext(ctx, ` + alter table media_file drop column library_id; + alter table album drop column library_id; + drop table library_artist; + drop table library; +`) + return err +} diff --git a/model/library.go b/model/library.go index d2270644..72ef1456 100644 --- a/model/library.go +++ b/model/library.go @@ -3,12 +3,17 @@ package model import ( "io/fs" "os" + "time" ) type Library struct { - ID int32 - Name string - Path string + ID int + Name string + Path string + RemotePath string + LastScanAt time.Time + UpdatedAt time.Time + CreatedAt time.Time } func (f Library) FS() fs.FS { @@ -18,6 +23,7 @@ func (f Library) FS() fs.FS { type Libraries []Library type LibraryRepository interface { - Get(id int32) (*Library, error) - GetAll() (Libraries, error) + Get(id int) (*Library, error) + Put(*Library) error + GetAll(...QueryOptions) (Libraries, error) } diff --git a/persistence/library_repository.go b/persistence/library_repository.go index 77bc5ace..eef2ef87 100644 --- a/persistence/library_repository.go +++ b/persistence/library_repository.go @@ -2,33 +2,57 @@ package persistence import ( "context" + "time" - "github.com/navidrome/navidrome/conf" + . "github.com/Masterminds/squirrel" "github.com/navidrome/navidrome/model" "github.com/pocketbase/dbx" ) type libraryRepository struct { - ctx context.Context + sqlRepository + sqlRestful } -func NewLibraryRepository(ctx context.Context, _ dbx.Builder) model.LibraryRepository { - return &libraryRepository{ctx} +func NewLibraryRepository(ctx context.Context, db dbx.Builder) model.LibraryRepository { + r := &libraryRepository{} + r.ctx = ctx + r.db = db + r.tableName = "library" + return r } -func (r *libraryRepository) Get(int32) (*model.Library, error) { - library := hardCoded() - return &library, nil +func (r *libraryRepository) Get(id int) (*model.Library, error) { + sq := r.newSelect().Columns("*").Where(Eq{"id": id}) + var res model.Library + err := r.queryOne(sq, &res) + return &res, err } -func (*libraryRepository) GetAll() (model.Libraries, error) { - return model.Libraries{hardCoded()}, nil +func (r *libraryRepository) Put(l *model.Library) error { + cols := map[string]any{ + "name": l.Name, + "path": l.Path, + "remote_path": l.RemotePath, + "last_scan_at": l.LastScanAt, + "updated_at": time.Now(), + } + if l.ID != 0 { + cols["id"] = l.ID + } + + sq := Insert(r.tableName).SetMap(cols). + Suffix(`ON CONFLICT(id) DO UPDATE set name = excluded.name, path = excluded.path, + remote_path = excluded.remote_path, last_scan_at = excluded.last_scan_at`) + _, err := r.executeSQL(sq) + return err } -func hardCoded() model.Library { - library := model.Library{ID: 0, Path: conf.Server.MusicFolder} - library.Name = "Music Library" - return library +func (r *libraryRepository) GetAll(ops ...model.QueryOptions) (model.Libraries, error) { + sq := r.newSelect(ops...).Columns("*") + res := model.Libraries{} + err := r.queryAll(sq, &res) + return res, err } var _ model.LibraryRepository = (*libraryRepository)(nil) diff --git a/server/initial_setup.go b/server/initial_setup.go index b399041f..af2430b1 100644 --- a/server/initial_setup.go +++ b/server/initial_setup.go @@ -16,6 +16,10 @@ import ( func initialSetup(ds model.DataStore) { _ = ds.WithTx(func(tx model.DataStore) error { + if err := createOrUpdateMusicFolder(ds); err != nil { + return err + } + properties := ds.Property(context.TODO()) _, err := properties.Get(consts.InitialSetupFlagKey) if err == nil { @@ -112,3 +116,12 @@ func checkExternalCredentials() { } } } + +func createOrUpdateMusicFolder(ds model.DataStore) error { + lib := model.Library{ID: 1, Name: "Music Library", Path: conf.Server.MusicFolder} + err := ds.Library(context.TODO()).Put(&lib) + if err != nil { + log.Error("Could not access Library table", err) + } + return err +} diff --git a/server/subsonic/browsing.go b/server/subsonic/browsing.go index b5061345..a60c662d 100644 --- a/server/subsonic/browsing.go +++ b/server/subsonic/browsing.go @@ -20,7 +20,7 @@ func (api *Router) GetMusicFolders(r *http.Request) (*responses.Subsonic, error) libraries, _ := api.ds.Library(r.Context()).GetAll() folders := make([]responses.MusicFolder, len(libraries)) for i, f := range libraries { - folders[i].Id = f.ID + folders[i].Id = int32(f.ID) folders[i].Name = f.Name } response := newResponse() @@ -30,7 +30,7 @@ func (api *Router) GetMusicFolders(r *http.Request) (*responses.Subsonic, error) func (api *Router) getArtistIndex(r *http.Request, libId int, ifModifiedSince time.Time) (*responses.Indexes, error) { ctx := r.Context() - folder, err := api.ds.Library(ctx).Get(int32(libId)) + folder, err := api.ds.Library(ctx).Get(libId) if err != nil { log.Error(ctx, "Error retrieving Library", "id", libId, err) return nil, err @@ -68,7 +68,7 @@ func (api *Router) getArtistIndex(r *http.Request, libId int, ifModifiedSince ti func (api *Router) GetIndexes(r *http.Request) (*responses.Subsonic, error) { p := req.Params(r) - musicFolderId := p.IntOr("musicFolderId", 0) + musicFolderId := p.IntOr("musicFolderId", 1) ifModifiedSince := p.TimeOr("ifModifiedSince", time.Time{}) res, err := api.getArtistIndex(r, musicFolderId, ifModifiedSince) @@ -83,7 +83,7 @@ func (api *Router) GetIndexes(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetArtists(r *http.Request) (*responses.Subsonic, error) { p := req.Params(r) - musicFolderId := p.IntOr("musicFolderId", 0) + musicFolderId := p.IntOr("musicFolderId", 1) res, err := api.getArtistIndex(r, musicFolderId, time.Time{}) if err != nil { return nil, err