diff --git a/.gitignore b/.gitignore index 2243b42c..7b97fcb2 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ navidrome.db-wal tags .gitinfo docker-compose.yml -!contrib/docker-compose.yml \ No newline at end of file +!contrib/docker-compose.yml +test-123.db diff --git a/core/playlists.go b/core/playlists.go index 0e19177c..2551ba0a 100644 --- a/core/playlists.go +++ b/core/playlists.go @@ -187,7 +187,7 @@ func (s *playlists) updatePlaylist(ctx context.Context, newPls *model.Playlist) newPls.Comment = pls.Comment newPls.OwnerID = pls.OwnerID newPls.Public = pls.Public - newPls.EvaluatedAt = time.Time{} + newPls.EvaluatedAt = &time.Time{} } else { log.Info(ctx, "Adding synced playlist", "playlist", newPls.Name, "path", newPls.Path, "owner", owner.UserName) newPls.OwnerID = owner.ID diff --git a/go.mod b/go.mod index 57ef4dbc..009c3a42 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/DexterLB/mpvipc v0.0.0-20230829142118-145d6eabdc37 github.com/Masterminds/squirrel v1.5.4 github.com/ReneKroon/ttlcache/v2 v2.11.0 - github.com/beego/beego/v2 v2.1.3 github.com/bradleyjkemp/cupaloy/v2 v2.8.0 github.com/deluan/rest v0.0.0-20211101235434-380523c4bb47 github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1 @@ -36,6 +35,7 @@ require ( github.com/mileusna/useragent v1.3.4 github.com/onsi/ginkgo/v2 v2.13.2 github.com/onsi/gomega v1.30.0 + github.com/pocketbase/dbx v1.10.1 github.com/pressly/goose/v3 v3.15.1 github.com/prometheus/client_golang v1.17.0 github.com/robfig/cron/v3 v3.0.1 @@ -69,7 +69,6 @@ require ( github.com/hajimehoshi/go-mp3 v0.3.4 // indirect github.com/hajimehoshi/oto v1.0.1 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/icza/bitio v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -94,14 +93,12 @@ require ( github.com/prometheus/procfs v0.12.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/segmentio/asm v1.2.0 // indirect - github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect github.com/spf13/afero v1.9.3 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect go.uber.org/goleak v1.1.11 // indirect golang.org/x/crypto v0.15.0 // indirect golang.org/x/exp/shiny v0.0.0-20230515195305-f3d0a9c9a5cc // indirect diff --git a/go.sum b/go.sum index 27f8d3da..745c6853 100644 --- a/go.sum +++ b/go.sum @@ -49,8 +49,6 @@ github.com/ReneKroon/ttlcache/v2 v2.11.0 h1:OvlcYFYi941SBN3v9dsDcC2N8vRxyHcCmJb3 github.com/ReneKroon/ttlcache/v2 v2.11.0/go.mod h1:mBxvsNY+BT8qLLd6CuAJubbKo6r0jh3nb5et22bbfGY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/beego/beego/v2 v2.1.3 h1:x436yz6jrSasYBzfOP39S097kvq5/5fBTFfEvVA456M= -github.com/beego/beego/v2 v2.1.3/go.mod h1:0J0RQVIpepnRUfu6ax+kLVVB1FcdYryHK9lpRl5wvbY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= @@ -124,6 +122,7 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= @@ -215,8 +214,6 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -262,8 +259,6 @@ github.com/lestrrat-go/jwx/v2 v2.0.17/go.mod h1:G8randPHLGAqhcNCqtt6/V/7E6fvJRl3 github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= @@ -302,6 +297,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pocketbase/dbx v1.10.1 h1:cw+vsyfCJD8YObOVeqb93YErnlxwYMkNZ4rwN0G0AaA= +github.com/pocketbase/dbx v1.10.1/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs= github.com/pressly/goose/v3 v3.15.1 h1:dKaJ1SdLvS/+HtS8PzFT0KBEtICC1jewLXM+b3emlv8= github.com/pressly/goose/v3 v3.15.1/go.mod h1:0E3Yg/+EwYzO6Rz2P98MlClFgIcoujbVRs575yi3iIM= github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= @@ -324,8 +321,6 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 h1:DAYUYH5869yV94zvCES9F51oYtN5oGlwjxJJz7ZCnik= -github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -364,8 +359,6 @@ github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8 github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/unrolled/secure v1.13.0 h1:sdr3Phw2+f8Px8HE5sd1EHdj1aV3yUwed/uZXChLFsk= github.com/unrolled/secure v1.13.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/model/album.go b/model/album.go index cae0f427..d4d89a19 100644 --- a/model/album.go +++ b/model/album.go @@ -10,14 +10,14 @@ import ( type Album struct { Annotations `structs:"-"` - ID string `structs:"id" json:"id" orm:"column(id)"` + ID string `structs:"id" json:"id"` Name string `structs:"name" json:"name"` EmbedArtPath string `structs:"embed_art_path" json:"embedArtPath"` - ArtistID string `structs:"artist_id" json:"artistId" orm:"column(artist_id)"` + ArtistID string `structs:"artist_id" json:"artistId"` Artist string `structs:"artist" json:"artist"` - AlbumArtistID string `structs:"album_artist_id" json:"albumArtistId" orm:"column(album_artist_id)"` + AlbumArtistID string `structs:"album_artist_id" json:"albumArtistId"` AlbumArtist string `structs:"album_artist" json:"albumArtist"` - AllArtistIDs string `structs:"all_artist_ids" json:"allArtistIds" orm:"column(all_artist_ids)"` + AllArtistIDs string `structs:"all_artist_ids" json:"allArtistIds"` MaxYear int `structs:"max_year" json:"maxYear"` MinYear int `structs:"min_year" json:"minYear"` Date string `structs:"date" json:"date,omitempty"` @@ -40,8 +40,8 @@ type Album struct { OrderAlbumName string `structs:"order_album_name" json:"orderAlbumName"` OrderAlbumArtistName string `structs:"order_album_artist_name" json:"orderAlbumArtistName"` CatalogNum string `structs:"catalog_num" json:"catalogNum,omitempty"` - MbzAlbumID string `structs:"mbz_album_id" json:"mbzAlbumId,omitempty" orm:"column(mbz_album_id)"` - MbzAlbumArtistID string `structs:"mbz_album_artist_id" json:"mbzAlbumArtistId,omitempty" orm:"column(mbz_album_artist_id)"` + MbzAlbumID string `structs:"mbz_album_id" json:"mbzAlbumId,omitempty"` + MbzAlbumArtistID string `structs:"mbz_album_artist_id" json:"mbzAlbumArtistId,omitempty"` MbzAlbumType string `structs:"mbz_album_type" json:"mbzAlbumType,omitempty"` MbzAlbumComment string `structs:"mbz_album_comment" json:"mbzAlbumComment,omitempty"` ImageFiles string `structs:"image_files" json:"imageFiles,omitempty"` @@ -50,7 +50,7 @@ type Album struct { SmallImageUrl string `structs:"small_image_url" json:"smallImageUrl,omitempty"` MediumImageUrl string `structs:"medium_image_url" json:"mediumImageUrl,omitempty"` LargeImageUrl string `structs:"large_image_url" json:"largeImageUrl,omitempty"` - ExternalUrl string `structs:"external_url" json:"externalUrl,omitempty" orm:"column(external_url)"` + ExternalUrl string `structs:"external_url" json:"externalUrl,omitempty"` ExternalInfoUpdatedAt time.Time `structs:"external_info_updated_at" json:"externalInfoUpdatedAt"` CreatedAt time.Time `structs:"created_at" json:"createdAt"` UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"` diff --git a/model/annotation.go b/model/annotation.go index b470da2a..f96e926c 100644 --- a/model/annotation.go +++ b/model/annotation.go @@ -3,11 +3,11 @@ package model import "time" type Annotations struct { - PlayCount int64 `structs:"-" json:"playCount"` - PlayDate time.Time `structs:"-" json:"playDate" ` - Rating int `structs:"-" json:"rating" ` - Starred bool `structs:"-" json:"starred" ` - StarredAt time.Time `structs:"-" json:"starredAt"` + PlayCount int64 `structs:"-" json:"playCount"` + PlayDate *time.Time `structs:"-" json:"playDate" ` + Rating int `structs:"-" json:"rating" ` + Starred bool `structs:"-" json:"starred" ` + StarredAt *time.Time `structs:"-" json:"starredAt"` } type AnnotatedRepository interface { diff --git a/model/artist.go b/model/artist.go index f6bb33f4..dedb402e 100644 --- a/model/artist.go +++ b/model/artist.go @@ -5,7 +5,7 @@ import "time" type Artist struct { Annotations `structs:"-"` - ID string `structs:"id" json:"id" orm:"column(id)"` + ID string `structs:"id" json:"id"` Name string `structs:"name" json:"name"` AlbumCount int `structs:"album_count" json:"albumCount"` SongCount int `structs:"song_count" json:"songCount"` @@ -14,13 +14,13 @@ type Artist struct { SortArtistName string `structs:"sort_artist_name" json:"sortArtistName,omitempty"` OrderArtistName string `structs:"order_artist_name" json:"orderArtistName"` Size int64 `structs:"size" json:"size"` - MbzArtistID string `structs:"mbz_artist_id" json:"mbzArtistId,omitempty" orm:"column(mbz_artist_id)"` + MbzArtistID string `structs:"mbz_artist_id" json:"mbzArtistId,omitempty"` Biography string `structs:"biography" json:"biography,omitempty"` SmallImageUrl string `structs:"small_image_url" json:"smallImageUrl,omitempty"` MediumImageUrl string `structs:"medium_image_url" json:"mediumImageUrl,omitempty"` LargeImageUrl string `structs:"large_image_url" json:"largeImageUrl,omitempty"` - ExternalUrl string `structs:"external_url" json:"externalUrl,omitempty" orm:"column(external_url)"` - SimilarArtists Artists `structs:"-" json:"-" orm:"-"` + ExternalUrl string `structs:"external_url" json:"externalUrl,omitempty"` + SimilarArtists Artists `structs:"similar_artists" json:"-"` ExternalInfoUpdatedAt time.Time `structs:"external_info_updated_at" json:"externalInfoUpdatedAt"` } diff --git a/model/genre.go b/model/genre.go index edb4a55c..3c3d6e55 100644 --- a/model/genre.go +++ b/model/genre.go @@ -1,7 +1,7 @@ package model type Genre struct { - ID string `structs:"id" json:"id" orm:"column(id)"` + ID string `structs:"id" json:"id"` Name string `structs:"name" json:"name"` SongCount int `structs:"-" json:"-"` AlbumCount int `structs:"-" json:"-"` diff --git a/model/mediafile.go b/model/mediafile.go index 71951d58..83f54be5 100644 --- a/model/mediafile.go +++ b/model/mediafile.go @@ -19,15 +19,15 @@ type MediaFile struct { Annotations `structs:"-"` Bookmarkable `structs:"-"` - ID string `structs:"id" json:"id" orm:"pk;column(id)"` + ID string `structs:"id" json:"id"` Path string `structs:"path" json:"path"` Title string `structs:"title" json:"title"` Album string `structs:"album" json:"album"` - ArtistID string `structs:"artist_id" json:"artistId" orm:"pk;column(artist_id)"` + ArtistID string `structs:"artist_id" json:"artistId"` Artist string `structs:"artist" json:"artist"` - AlbumArtistID string `structs:"album_artist_id" json:"albumArtistId" orm:"pk;column(album_artist_id)"` + AlbumArtistID string `structs:"album_artist_id" json:"albumArtistId"` AlbumArtist string `structs:"album_artist" json:"albumArtist"` - AlbumID string `structs:"album_id" json:"albumId" orm:"pk;column(album_id)"` + AlbumID string `structs:"album_id" json:"albumId"` HasCoverArt bool `structs:"has_cover_art" json:"hasCoverArt"` TrackNumber int `structs:"track_number" json:"trackNumber"` DiscNumber int `structs:"disc_number" json:"discNumber"` @@ -59,17 +59,17 @@ type MediaFile struct { Lyrics string `structs:"lyrics" json:"lyrics,omitempty"` Bpm int `structs:"bpm" json:"bpm,omitempty"` CatalogNum string `structs:"catalog_num" json:"catalogNum,omitempty"` - MbzRecordingID string `structs:"mbz_recording_id" json:"mbzRecordingID,omitempty" orm:"column(mbz_recording_id)"` - MbzReleaseTrackID string `structs:"mbz_release_track_id" json:"mbzReleaseTrackId,omitempty" orm:"column(mbz_release_track_id)"` - MbzAlbumID string `structs:"mbz_album_id" json:"mbzAlbumId,omitempty" orm:"column(mbz_album_id)"` - MbzArtistID string `structs:"mbz_artist_id" json:"mbzArtistId,omitempty" orm:"column(mbz_artist_id)"` - MbzAlbumArtistID string `structs:"mbz_album_artist_id" json:"mbzAlbumArtistId,omitempty" orm:"column(mbz_album_artist_id)"` + MbzRecordingID string `structs:"mbz_recording_id" json:"mbzRecordingID,omitempty"` + MbzReleaseTrackID string `structs:"mbz_release_track_id" json:"mbzReleaseTrackId,omitempty"` + MbzAlbumID string `structs:"mbz_album_id" json:"mbzAlbumId,omitempty"` + MbzArtistID string `structs:"mbz_artist_id" json:"mbzArtistId,omitempty"` + MbzAlbumArtistID string `structs:"mbz_album_artist_id" json:"mbzAlbumArtistId,omitempty"` MbzAlbumType string `structs:"mbz_album_type" json:"mbzAlbumType,omitempty"` MbzAlbumComment string `structs:"mbz_album_comment" json:"mbzAlbumComment,omitempty"` - RGAlbumGain float64 `structs:"rg_album_gain" json:"rgAlbumGain" orm:"column(rg_album_gain)"` - RGAlbumPeak float64 `structs:"rg_album_peak" json:"rgAlbumPeak" orm:"column(rg_album_peak)"` - RGTrackGain float64 `structs:"rg_track_gain" json:"rgTrackGain" orm:"column(rg_track_gain)"` - RGTrackPeak float64 `structs:"rg_track_peak" json:"rgTrackPeak" orm:"column(rg_track_peak)"` + RGAlbumGain float64 `structs:"rg_album_gain" json:"rgAlbumGain"` + RGAlbumPeak float64 `structs:"rg_album_peak" json:"rgAlbumPeak"` + RGTrackGain float64 `structs:"rg_track_gain" json:"rgTrackGain"` + RGTrackPeak float64 `structs:"rg_track_peak" json:"rgTrackPeak"` CreatedAt time.Time `structs:"created_at" json:"createdAt"` // Time this entry was created in the DB UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"` // Time of file last update (mtime) diff --git a/model/player.go b/model/player.go index 372ad570..cbbad24e 100644 --- a/model/player.go +++ b/model/player.go @@ -5,7 +5,7 @@ import ( ) type Player struct { - ID string `structs:"id" json:"id" orm:"column(id)"` + ID string `structs:"id" json:"id"` Name string `structs:"name" json:"name"` UserAgent string `structs:"user_agent" json:"userAgent"` UserName string `structs:"user_name" json:"userName"` diff --git a/model/playlist.go b/model/playlist.go index f9f7288d..e556aeb9 100644 --- a/model/playlist.go +++ b/model/playlist.go @@ -11,14 +11,14 @@ import ( ) type Playlist struct { - ID string `structs:"id" json:"id" orm:"column(id)"` + ID string `structs:"id" json:"id"` Name string `structs:"name" json:"name"` Comment string `structs:"comment" json:"comment"` Duration float32 `structs:"duration" json:"duration"` Size int64 `structs:"size" json:"size"` SongCount int `structs:"song_count" json:"songCount"` OwnerName string `structs:"-" json:"ownerName"` - OwnerID string `structs:"owner_id" json:"ownerId" orm:"column(owner_id)"` + OwnerID string `structs:"owner_id" json:"ownerId"` Public bool `structs:"public" json:"public"` Tracks PlaylistTracks `structs:"-" json:"tracks,omitempty"` Path string `structs:"path" json:"path"` @@ -27,8 +27,8 @@ type Playlist struct { UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"` // SmartPlaylist attributes - Rules *criteria.Criteria `structs:"-" json:"rules"` - EvaluatedAt time.Time `structs:"evaluated_at" json:"evaluatedAt"` + Rules *criteria.Criteria `structs:"rules" json:"rules"` + EvaluatedAt *time.Time `structs:"evaluated_at" json:"evaluatedAt"` } func (pls Playlist) IsSmartPlaylist() bool { @@ -114,9 +114,9 @@ type PlaylistRepository interface { } type PlaylistTrack struct { - ID string `json:"id" orm:"column(id)"` - MediaFileID string `json:"mediaFileId" orm:"column(media_file_id)"` - PlaylistID string `json:"playlistId" orm:"column(playlist_id)"` + ID string `json:"id"` + MediaFileID string `json:"mediaFileId"` + PlaylistID string `json:"playlistId"` MediaFile } diff --git a/model/playqueue.go b/model/playqueue.go index 3c9786fd..52ba173d 100644 --- a/model/playqueue.go +++ b/model/playqueue.go @@ -5,8 +5,8 @@ import ( ) type PlayQueue struct { - ID string `structs:"id" json:"id" orm:"column(id)"` - UserID string `structs:"user_id" json:"userId" orm:"column(user_id)"` + ID string `structs:"id" json:"id"` + UserID string `structs:"user_id" json:"userId"` Current string `structs:"current" json:"current"` Position int64 `structs:"position" json:"position"` ChangedBy string `structs:"changed_by" json:"changedBy"` diff --git a/model/radio.go b/model/radio.go index b0706577..567d32e4 100644 --- a/model/radio.go +++ b/model/radio.go @@ -3,10 +3,10 @@ package model import "time" type Radio struct { - ID string `structs:"id" json:"id" orm:"pk;column(id)"` + ID string `structs:"id" json:"id"` StreamUrl string `structs:"stream_url" json:"streamUrl"` Name string `structs:"name" json:"name"` - HomePageUrl string `structs:"home_page_url" json:"homePageUrl" orm:"column(home_page_url)"` + HomePageUrl string `structs:"home_page_url" json:"homePageUrl"` CreatedAt time.Time `structs:"created_at" json:"createdAt"` UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"` } diff --git a/model/scrobble_buffer.go b/model/scrobble_buffer.go index 9b2c754a..4714a2fe 100644 --- a/model/scrobble_buffer.go +++ b/model/scrobble_buffer.go @@ -5,7 +5,7 @@ import "time" type ScrobbleEntry struct { MediaFile Service string - UserID string `structs:"user_id" orm:"column(user_id)"` + UserID string `structs:"user_id"` PlayTime time.Time EnqueueTime time.Time } diff --git a/model/share.go b/model/share.go index 4486fdf4..d8bc3bdd 100644 --- a/model/share.go +++ b/model/share.go @@ -8,14 +8,14 @@ import ( ) type Share struct { - ID string `structs:"id" json:"id,omitempty" orm:"column(id)"` - UserID string `structs:"user_id" json:"userId,omitempty" orm:"column(user_id)"` - Username string `structs:"-" json:"username,omitempty" orm:"-"` + ID string `structs:"id" json:"id,omitempty"` + UserID string `structs:"user_id" json:"userId,omitempty"` + Username string `structs:"-" json:"username,omitempty"` Description string `structs:"description" json:"description,omitempty"` Downloadable bool `structs:"downloadable" json:"downloadable"` ExpiresAt time.Time `structs:"expires_at" json:"expiresAt,omitempty"` LastVisitedAt time.Time `structs:"last_visited_at" json:"lastVisitedAt,omitempty"` - ResourceIDs string `structs:"resource_ids" json:"resourceIds,omitempty" orm:"column(resource_ids)"` + ResourceIDs string `structs:"resource_ids" json:"resourceIds,omitempty"` ResourceType string `structs:"resource_type" json:"resourceType,omitempty"` Contents string `structs:"contents" json:"contents,omitempty"` Format string `structs:"format" json:"format,omitempty"` @@ -23,10 +23,10 @@ type Share struct { VisitCount int `structs:"visit_count" json:"visitCount,omitempty"` CreatedAt time.Time `structs:"created_at" json:"createdAt,omitempty"` UpdatedAt time.Time `structs:"updated_at" json:"updatedAt,omitempty"` - Tracks MediaFiles `structs:"-" json:"tracks,omitempty" orm:"-"` - Albums Albums `structs:"-" json:"albums,omitempty" orm:"-"` - URL string `structs:"-" json:"-" orm:"-"` - ImageURL string `structs:"-" json:"-" orm:"-"` + Tracks MediaFiles `structs:"-" json:"tracks,omitempty"` + Albums Albums `structs:"-" json:"albums,omitempty"` + URL string `structs:"-" json:"-"` + ImageURL string `structs:"-" json:"-"` } func (s Share) CoverArtID() ArtworkID { diff --git a/model/transcoding.go b/model/transcoding.go index 64dba662..9b81a7c9 100644 --- a/model/transcoding.go +++ b/model/transcoding.go @@ -1,7 +1,7 @@ package model type Transcoding struct { - ID string `structs:"id" json:"id" orm:"column(id)"` + ID string `structs:"id" json:"id"` Name string `structs:"name" json:"name"` TargetFormat string `structs:"target_format" json:"targetFormat"` Command string `structs:"command" json:"command"` diff --git a/model/user.go b/model/user.go index fa0a42ea..7c41ac04 100644 --- a/model/user.go +++ b/model/user.go @@ -3,15 +3,15 @@ package model import "time" type User struct { - ID string `structs:"id" json:"id" orm:"column(id)"` - UserName string `structs:"user_name" json:"userName"` - Name string `structs:"name" json:"name"` - Email string `structs:"email" json:"email"` - IsAdmin bool `structs:"is_admin" json:"isAdmin"` - LastLoginAt time.Time `structs:"last_login_at" json:"lastLoginAt"` - LastAccessAt time.Time `structs:"last_access_at" json:"lastAccessAt"` - CreatedAt time.Time `structs:"created_at" json:"createdAt"` - UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"` + ID string `structs:"id" json:"id"` + UserName string `structs:"user_name" json:"userName"` + Name string `structs:"name" json:"name"` + Email string `structs:"email" json:"email"` + IsAdmin bool `structs:"is_admin" json:"isAdmin"` + LastLoginAt *time.Time `structs:"last_login_at" json:"lastLoginAt"` + LastAccessAt *time.Time `structs:"last_access_at" json:"lastAccessAt"` + CreatedAt time.Time `structs:"created_at" json:"createdAt"` + UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"` // This is only available on the backend, and it is never sent over the wire Password string `structs:"-" json:"-"` diff --git a/persistence/album_repository.go b/persistence/album_repository.go index e2eae06d..62d01b7b 100644 --- a/persistence/album_repository.go +++ b/persistence/album_repository.go @@ -6,11 +6,11 @@ import ( "strings" . "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/deluan/rest" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" + "github.com/pocketbase/dbx" ) type albumRepository struct { @@ -18,10 +18,10 @@ type albumRepository struct { sqlRestful } -func NewAlbumRepository(ctx context.Context, o orm.QueryExecutor) model.AlbumRepository { +func NewAlbumRepository(ctx context.Context, db dbx.Builder) model.AlbumRepository { r := &albumRepository{} r.ctx = ctx - r.ormer = o + r.db = db r.tableName = "album" r.sortMappings = map[string]string{ "name": "order_album_name asc, order_album_artist_name asc", diff --git a/persistence/album_repository_test.go b/persistence/album_repository_test.go index e7b9fafc..6ab33ad2 100644 --- a/persistence/album_repository_test.go +++ b/persistence/album_repository_test.go @@ -3,7 +3,6 @@ package persistence import ( "context" - "github.com/beego/beego/v2/client/orm" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" @@ -16,7 +15,7 @@ var _ = Describe("AlbumRepository", func() { BeforeEach(func() { ctx := request.WithUser(log.NewContext(context.TODO()), model.User{ID: "userid", UserName: "johndoe"}) - repo = NewAlbumRepository(ctx, orm.NewOrm()) + repo = NewAlbumRepository(ctx, getDBXBuilder()) }) Describe("Get", func() { diff --git a/persistence/artist_repository.go b/persistence/artist_repository.go index 9b0d7231..ff75109e 100644 --- a/persistence/artist_repository.go +++ b/persistence/artist_repository.go @@ -8,13 +8,13 @@ import ( "strings" . "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/deluan/rest" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/utils" + "github.com/pocketbase/dbx" ) type artistRepository struct { @@ -24,14 +24,40 @@ type artistRepository struct { } type dbArtist struct { - model.Artist `structs:",flatten"` - SimilarArtists string `structs:"similar_artists" json:"similarArtists"` + *model.Artist `structs:",flatten"` + SimilarArtists string `structs:"-" json:"similarArtists"` } -func NewArtistRepository(ctx context.Context, o orm.QueryExecutor) model.ArtistRepository { +func (a *dbArtist) PostScan() error { + if a.SimilarArtists == "" { + return nil + } + for _, s := range strings.Split(a.SimilarArtists, ";") { + fields := strings.Split(s, ":") + if len(fields) != 2 { + continue + } + name, _ := url.QueryUnescape(fields[1]) + a.Artist.SimilarArtists = append(a.Artist.SimilarArtists, model.Artist{ + ID: fields[0], + Name: name, + }) + } + return nil +} +func (a *dbArtist) PostMapArgs(m map[string]any) error { + var sa []string + for _, s := range a.Artist.SimilarArtists { + sa = append(sa, fmt.Sprintf("%s:%s", s.ID, url.QueryEscape(s.Name))) + } + m["similar_artists"] = strings.Join(sa, ";") + return nil +} + +func NewArtistRepository(ctx context.Context, db dbx.Builder) model.ArtistRepository { r := &artistRepository{} r.ctx = ctx - r.ormer = o + r.db = db r.indexGroups = utils.ParseIndexGroups(conf.Server.IndexGroups) r.tableName = "artist" r.sortMappings = map[string]string{ @@ -62,7 +88,7 @@ func (r *artistRepository) Exists(id string) (bool, error) { func (r *artistRepository) Put(a *model.Artist, colsToUpdate ...string) error { a.FullText = getFullText(a.Name, a.SortArtistName) - dba := r.fromModel(a) + dba := &dbArtist{Artist: a} _, err := r.put(dba.ID, dba, colsToUpdate...) if err != nil { return err @@ -102,41 +128,11 @@ func (r *artistRepository) GetAll(options ...model.QueryOptions) (model.Artists, func (r *artistRepository) toModels(dba []dbArtist) model.Artists { res := model.Artists{} for i := range dba { - a := dba[i] - res = append(res, *r.toModel(&a)) + res = append(res, *dba[i].Artist) } return res } -func (r *artistRepository) toModel(dba *dbArtist) *model.Artist { - a := dba.Artist - a.SimilarArtists = nil - for _, s := range strings.Split(dba.SimilarArtists, ";") { - fields := strings.Split(s, ":") - if len(fields) != 2 { - continue - } - name, _ := url.QueryUnescape(fields[1]) - a.SimilarArtists = append(a.SimilarArtists, model.Artist{ - ID: fields[0], - Name: name, - }) - } - return &a -} - -func (r *artistRepository) fromModel(a *model.Artist) *dbArtist { - dba := &dbArtist{Artist: *a} - var sa []string - - for _, s := range a.SimilarArtists { - sa = append(sa, fmt.Sprintf("%s:%s", s.ID, url.QueryEscape(s.Name))) - } - - dba.SimilarArtists = strings.Join(sa, ";") - return dba -} - func (r *artistRepository) getIndexKey(a *model.Artist) string { name := strings.ToLower(utils.NoArticle(a.Name)) for k, v := range r.indexGroups { diff --git a/persistence/artist_repository_test.go b/persistence/artist_repository_test.go index 6d35191b..5e0304ef 100644 --- a/persistence/artist_repository_test.go +++ b/persistence/artist_repository_test.go @@ -3,7 +3,7 @@ package persistence import ( "context" - "github.com/beego/beego/v2/client/orm" + "github.com/fatih/structs" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" @@ -18,7 +18,7 @@ var _ = Describe("ArtistRepository", func() { BeforeEach(func() { ctx := log.NewContext(context.TODO()) ctx = request.WithUser(ctx, model.User{ID: "userid"}) - repo = NewArtistRepository(ctx, orm.NewOrm()) + repo = NewArtistRepository(ctx, getDBXBuilder()) }) Describe("Count", func() { @@ -71,8 +71,17 @@ var _ = Describe("ArtistRepository", func() { }} }) It("maps fields", func() { - dba := repo.(*artistRepository).fromModel(a) - actual := repo.(*artistRepository).toModel(dba) + dba := &dbArtist{Artist: a} + m := structs.Map(dba) + Expect(dba.PostMapArgs(m)).To(Succeed()) + Expect(m).To(HaveKeyWithValue("similar_artists", "2:AC%2FDC;-1:Test%3BWith%3ASep%2CChars")) + + other := dbArtist{SimilarArtists: m["similar_artists"].(string), Artist: &model.Artist{ + ID: "1", Name: "Van Halen", + }} + Expect(other.PostScan()).To(Succeed()) + + actual := other.Artist Expect(*actual).To(MatchFields(IgnoreExtras, Fields{ "ID": Equal(a.ID), "Name": Equal(a.Name), diff --git a/persistence/genre_repository.go b/persistence/genre_repository.go index fe6c6f52..11a5cef1 100644 --- a/persistence/genre_repository.go +++ b/persistence/genre_repository.go @@ -4,9 +4,9 @@ import ( "context" "github.com/google/uuid" + "github.com/pocketbase/dbx" . "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/deluan/rest" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" @@ -17,10 +17,10 @@ type genreRepository struct { sqlRestful } -func NewGenreRepository(ctx context.Context, o orm.QueryExecutor) model.GenreRepository { +func NewGenreRepository(ctx context.Context, db dbx.Builder) model.GenreRepository { r := &genreRepository{} r.ctx = ctx - r.ormer = o + r.db = db r.tableName = "genre" r.filterMappings = map[string]filterFunc{ "name": containsFilter, @@ -29,7 +29,12 @@ func NewGenreRepository(ctx context.Context, o orm.QueryExecutor) model.GenreRep } func (r *genreRepository) GetAll(opt ...model.QueryOptions) (model.Genres, error) { - sq := r.newSelect(opt...).Columns("genre.id", "genre.name", "a.album_count", "m.song_count"). + sq := r.newSelect(opt...).Columns( + "genre.id", + "genre.name", + "coalesce(a.album_count, 0) as album_count", + "coalesce(m.song_count, 0) as song_count", + ). LeftJoin("(select ag.genre_id, count(ag.album_id) as album_count from album_genres ag group by ag.genre_id) a on a.genre_id = genre.id"). LeftJoin("(select mg.genre_id, count(mg.media_file_id) as song_count from media_file_genres mg group by mg.genre_id) m on m.genre_id = genre.id") res := model.Genres{} diff --git a/persistence/genre_repository_test.go b/persistence/genre_repository_test.go index 212e1a1b..971bdfc1 100644 --- a/persistence/genre_repository_test.go +++ b/persistence/genre_repository_test.go @@ -3,26 +3,27 @@ package persistence_test import ( "context" - "github.com/beego/beego/v2/client/orm" "github.com/google/uuid" + "github.com/navidrome/navidrome/db" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/persistence" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/pocketbase/dbx" ) var _ = Describe("GenreRepository", func() { var repo model.GenreRepository BeforeEach(func() { - repo = persistence.NewGenreRepository(log.NewContext(context.TODO()), orm.NewOrm()) + repo = persistence.NewGenreRepository(log.NewContext(context.TODO()), dbx.NewFromDB(db.Db(), db.Driver)) }) Describe("GetAll()", func() { It("returns all records", func() { genres, err := repo.GetAll() - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) Expect(genres).To(ConsistOf( model.Genre{ID: "gn-1", Name: "Electronic", AlbumCount: 1, SongCount: 2}, model.Genre{ID: "gn-2", Name: "Rock", AlbumCount: 3, SongCount: 3}, @@ -43,13 +44,14 @@ var _ = Describe("GenreRepository", func() { It("insert non-existent genre names", func() { g := model.Genre{Name: "Reggae"} err := repo.Put(&g) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) // ID is a uuid _, err = uuid.Parse(g.ID) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) - genres, _ := repo.GetAll() + genres, err := repo.GetAll() + Expect(err).ToNot(HaveOccurred()) Expect(genres).To(HaveLen(3)) Expect(genres).To(ContainElement(model.Genre{ID: g.ID, Name: "Reggae", AlbumCount: 0, SongCount: 0})) }) diff --git a/persistence/helpers.go b/persistence/helpers.go index 4d756dce..4a62955f 100644 --- a/persistence/helpers.go +++ b/persistence/helpers.go @@ -1,6 +1,7 @@ package persistence import ( + "database/sql/driver" "fmt" "regexp" "strings" @@ -10,14 +11,32 @@ import ( "github.com/fatih/structs" ) -func toSqlArgs(rec interface{}) (map[string]interface{}, error) { +type PostMapper interface { + PostMapArgs(map[string]any) error +} + +func toSQLArgs(rec interface{}) (map[string]interface{}, error) { m := structs.Map(rec) for k, v := range m { - if t, ok := v.(time.Time); ok { + switch t := v.(type) { + case time.Time: m[k] = t.Format(time.RFC3339Nano) + case *time.Time: + if t != nil { + m[k] = t.Format(time.RFC3339Nano) + } + case driver.Valuer: + var err error + m[k], err = t.Value() + if err != nil { + return nil, err + } } - if t, ok := v.(*time.Time); ok && t != nil { - m[k] = t.Format(time.RFC3339Nano) + } + if r, ok := rec.(PostMapper); ok { + err := r.PostMapArgs(m) + if err != nil { + return nil, err } } return m, nil diff --git a/persistence/helpers_test.go b/persistence/helpers_test.go index ba3d1c49..a47c6008 100644 --- a/persistence/helpers_test.go +++ b/persistence/helpers_test.go @@ -23,7 +23,7 @@ var _ = Describe("Helpers", func() { Expect(toSnakeCase("snake_case")).To(Equal("snake_case")) }) }) - Describe("toSqlArgs", func() { + Describe("toSQLArgs", func() { type Embed struct{} type Model struct { Embed `structs:"-"` @@ -37,10 +37,11 @@ var _ = Describe("Helpers", func() { It("returns a map with snake_case keys", func() { now := time.Now() m := &Model{ID: "123", AlbumId: "456", CreatedAt: now, UpdatedAt: &now, PlayCount: 2} - args, err := toSqlArgs(m) + args, err := toSQLArgs(m) Expect(err).To(BeNil()) Expect(args).To(HaveKeyWithValue("id", "123")) Expect(args).To(HaveKeyWithValue("album_id", "456")) + Expect(args).To(HaveKeyWithValue("play_count", 2)) Expect(args).To(HaveKeyWithValue("updated_at", now.Format(time.RFC3339Nano))) Expect(args).To(HaveKeyWithValue("created_at", now.Format(time.RFC3339Nano))) Expect(args).ToNot(HaveKey("Embed")) diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go index c0c1b4a5..54a66ee4 100644 --- a/persistence/mediafile_repository.go +++ b/persistence/mediafile_repository.go @@ -9,10 +9,10 @@ import ( "unicode/utf8" . "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/deluan/rest" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" + "github.com/pocketbase/dbx" ) type mediaFileRepository struct { @@ -20,10 +20,10 @@ type mediaFileRepository struct { sqlRestful } -func NewMediaFileRepository(ctx context.Context, o orm.QueryExecutor) *mediaFileRepository { +func NewMediaFileRepository(ctx context.Context, db dbx.Builder) *mediaFileRepository { r := &mediaFileRepository{} r.ctx = ctx - r.ormer = o + r.db = db r.tableName = "media_file" r.sortMappings = map[string]string{ "artist": "order_artist_name asc, order_album_name asc, release_date asc, disc_number asc, track_number asc", @@ -146,7 +146,7 @@ func (r *mediaFileRepository) FindPathsRecursively(basePath string) ([]string, e sel := r.newSelect().Columns(fmt.Sprintf("distinct rtrim(path, replace(path, '%s', ''))", string(os.PathSeparator))). Where(pathStartsWith(path)) var res []string - err := r.queryAll(sel, &res) + err := r.queryAllSlice(sel, &res) return res, err } diff --git a/persistence/mediafile_repository_test.go b/persistence/mediafile_repository_test.go index e6fa84f1..4310787a 100644 --- a/persistence/mediafile_repository_test.go +++ b/persistence/mediafile_repository_test.go @@ -5,7 +5,6 @@ import ( "time" "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/google/uuid" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" @@ -20,7 +19,7 @@ var _ = Describe("MediaRepository", func() { BeforeEach(func() { ctx := log.NewContext(context.TODO()) ctx = request.WithUser(ctx, model.User{ID: "userid"}) - mr = NewMediaFileRepository(ctx, orm.NewOrm()) + mr = NewMediaFileRepository(ctx, getDBXBuilder()) }) It("gets mediafile from the DB", func() { diff --git a/persistence/mediafolders_repository.go b/persistence/mediafolders_repository.go index f858c5ad..3e86a6a2 100644 --- a/persistence/mediafolders_repository.go +++ b/persistence/mediafolders_repository.go @@ -3,16 +3,16 @@ package persistence import ( "context" - "github.com/beego/beego/v2/client/orm" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/model" + "github.com/pocketbase/dbx" ) type mediaFolderRepository struct { ctx context.Context } -func NewMediaFolderRepository(ctx context.Context, o orm.QueryExecutor) model.MediaFolderRepository { +func NewMediaFolderRepository(ctx context.Context, _ dbx.Builder) model.MediaFolderRepository { return &mediaFolderRepository{ctx} } diff --git a/persistence/persistence.go b/persistence/persistence.go index 277cd965..fc670933 100644 --- a/persistence/persistence.go +++ b/persistence/persistence.go @@ -5,79 +5,78 @@ import ( "database/sql" "reflect" - "github.com/beego/beego/v2/client/orm" "github.com/navidrome/navidrome/db" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" + "github.com/pocketbase/dbx" ) type SQLStore struct { - orm orm.QueryExecutor - db *sql.DB + db dbx.Builder } -func New(db *sql.DB) model.DataStore { - return &SQLStore{db: db} +func New(conn *sql.DB) model.DataStore { + return &SQLStore{db: dbx.NewFromDB(conn, db.Driver)} } func (s *SQLStore) Album(ctx context.Context) model.AlbumRepository { - return NewAlbumRepository(ctx, s.getOrmer()) + return NewAlbumRepository(ctx, s.getDBXBuilder()) } func (s *SQLStore) Artist(ctx context.Context) model.ArtistRepository { - return NewArtistRepository(ctx, s.getOrmer()) + return NewArtistRepository(ctx, s.getDBXBuilder()) } func (s *SQLStore) MediaFile(ctx context.Context) model.MediaFileRepository { - return NewMediaFileRepository(ctx, s.getOrmer()) + return NewMediaFileRepository(ctx, s.getDBXBuilder()) } func (s *SQLStore) MediaFolder(ctx context.Context) model.MediaFolderRepository { - return NewMediaFolderRepository(ctx, s.getOrmer()) + return NewMediaFolderRepository(ctx, s.getDBXBuilder()) } func (s *SQLStore) Genre(ctx context.Context) model.GenreRepository { - return NewGenreRepository(ctx, s.getOrmer()) + return NewGenreRepository(ctx, s.getDBXBuilder()) } func (s *SQLStore) PlayQueue(ctx context.Context) model.PlayQueueRepository { - return NewPlayQueueRepository(ctx, s.getOrmer()) + return NewPlayQueueRepository(ctx, s.getDBXBuilder()) } func (s *SQLStore) Playlist(ctx context.Context) model.PlaylistRepository { - return NewPlaylistRepository(ctx, s.getOrmer()) + return NewPlaylistRepository(ctx, s.getDBXBuilder()) } func (s *SQLStore) Property(ctx context.Context) model.PropertyRepository { - return NewPropertyRepository(ctx, s.getOrmer()) + return NewPropertyRepository(ctx, s.getDBXBuilder()) } func (s *SQLStore) Radio(ctx context.Context) model.RadioRepository { - return NewRadioRepository(ctx, s.getOrmer()) + return NewRadioRepository(ctx, s.getDBXBuilder()) } func (s *SQLStore) UserProps(ctx context.Context) model.UserPropsRepository { - return NewUserPropsRepository(ctx, s.getOrmer()) + return NewUserPropsRepository(ctx, s.getDBXBuilder()) } func (s *SQLStore) Share(ctx context.Context) model.ShareRepository { - return NewShareRepository(ctx, s.getOrmer()) + return NewShareRepository(ctx, s.getDBXBuilder()) } func (s *SQLStore) User(ctx context.Context) model.UserRepository { - return NewUserRepository(ctx, s.getOrmer()) + return NewUserRepository(ctx, s.getDBXBuilder()) } func (s *SQLStore) Transcoding(ctx context.Context) model.TranscodingRepository { - return NewTranscodingRepository(ctx, s.getOrmer()) + return NewTranscodingRepository(ctx, s.getDBXBuilder()) } func (s *SQLStore) Player(ctx context.Context) model.PlayerRepository { - return NewPlayerRepository(ctx, s.getOrmer()) + return NewPlayerRepository(ctx, s.getDBXBuilder()) } func (s *SQLStore) ScrobbleBuffer(ctx context.Context) model.ScrobbleBufferRepository { - return NewScrobbleBufferRepository(ctx, s.getOrmer()) + return NewScrobbleBufferRepository(ctx, s.getDBXBuilder()) } func (s *SQLStore) Resource(ctx context.Context, m interface{}) model.ResourceRepository { @@ -108,12 +107,12 @@ func (s *SQLStore) Resource(ctx context.Context, m interface{}) model.ResourceRe } func (s *SQLStore) WithTx(block func(tx model.DataStore) error) error { - o, err := orm.NewOrmWithDB(db.Driver, "default", s.db) - if err != nil { - return err + conn, ok := s.db.(*dbx.DB) + if !ok { + conn = dbx.NewFromDB(db.Db(), db.Driver) } - return o.DoTx(func(ctx context.Context, txOrm orm.TxOrmer) error { - newDb := &SQLStore{orm: txOrm} + return conn.Transactional(func(tx *dbx.Tx) error { + newDb := &SQLStore{db: tx} return block(newDb) }) } @@ -171,13 +170,9 @@ func (s *SQLStore) GC(ctx context.Context, rootFolder string) error { return err } -func (s *SQLStore) getOrmer() orm.QueryExecutor { - if s.orm == nil { - o, err := orm.NewOrmWithDB(db.Driver, "default", s.db) - if err != nil { - log.Error("Error obtaining new orm instance", err) - } - return o +func (s *SQLStore) getDBXBuilder() dbx.Builder { + if s.db == nil { + return dbx.NewFromDB(db.Db(), db.Driver) } - return s.orm + return s.db } diff --git a/persistence/persistence_suite_test.go b/persistence/persistence_suite_test.go index e0f03388..bdfd31f3 100644 --- a/persistence/persistence_suite_test.go +++ b/persistence/persistence_suite_test.go @@ -5,7 +5,6 @@ import ( "path/filepath" "testing" - "github.com/beego/beego/v2/client/orm" _ "github.com/mattn/go-sqlite3" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/db" @@ -15,6 +14,7 @@ import ( "github.com/navidrome/navidrome/tests" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/pocketbase/dbx" ) func TestPersistence(t *testing.T) { @@ -23,13 +23,16 @@ func TestPersistence(t *testing.T) { //os.Remove("./test-123.db") //conf.Server.DbPath = "./test-123.db" conf.Server.DbPath = "file::memory:?cache=shared" - _ = orm.RegisterDataBase("default", db.Driver, conf.Server.DbPath) db.Init() log.SetLevel(log.LevelError) RegisterFailHandler(Fail) RunSpecs(t, "Persistence Suite") } +func getDBXBuilder() *dbx.DB { + return dbx.NewFromDB(db.Db(), db.Driver) +} + var ( genreElectronic = model.Genre{ID: "gn-1", Name: "Electronic"} genreRock = model.Genre{ID: "gn-2", Name: "Rock"} @@ -88,18 +91,18 @@ func P(path string) string { // Initialize test DB // TODO Load this data setup from file(s) var _ = BeforeSuite(func() { - o := orm.NewOrm() + conn := getDBXBuilder() ctx := log.NewContext(context.TODO()) user := model.User{ID: "userid", UserName: "userid", IsAdmin: true} ctx = request.WithUser(ctx, user) - ur := NewUserRepository(ctx, o) + ur := NewUserRepository(ctx, conn) err := ur.Put(&user) if err != nil { panic(err) } - gr := NewGenreRepository(ctx, o) + gr := NewGenreRepository(ctx, conn) for i := range testGenres { g := testGenres[i] err := gr.Put(&g) @@ -108,7 +111,7 @@ var _ = BeforeSuite(func() { } } - mr := NewMediaFileRepository(ctx, o) + mr := NewMediaFileRepository(ctx, conn) for i := range testSongs { s := testSongs[i] err := mr.Put(&s) @@ -117,7 +120,7 @@ var _ = BeforeSuite(func() { } } - alr := NewAlbumRepository(ctx, o).(*albumRepository) + alr := NewAlbumRepository(ctx, conn).(*albumRepository) for i := range testAlbums { a := testAlbums[i] err := alr.Put(&a) @@ -126,7 +129,7 @@ var _ = BeforeSuite(func() { } } - arr := NewArtistRepository(ctx, o) + arr := NewArtistRepository(ctx, conn) for i := range testArtists { a := testArtists[i] err := arr.Put(&a) @@ -135,7 +138,7 @@ var _ = BeforeSuite(func() { } } - rar := NewRadioRepository(ctx, o) + rar := NewRadioRepository(ctx, conn) for i := range testRadios { r := testRadios[i] err := rar.Put(&r) @@ -157,7 +160,7 @@ var _ = BeforeSuite(func() { plsCool.AddTracks([]string{"1004"}) testPlaylists = []*model.Playlist{&plsBest, &plsCool} - pr := NewPlaylistRepository(ctx, o) + pr := NewPlaylistRepository(ctx, conn) for i := range testPlaylists { err := pr.Put(testPlaylists[i]) if err != nil { diff --git a/persistence/player_repository.go b/persistence/player_repository.go index 7e7eff4c..ea28e2c4 100644 --- a/persistence/player_repository.go +++ b/persistence/player_repository.go @@ -5,9 +5,9 @@ import ( "errors" . "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/deluan/rest" "github.com/navidrome/navidrome/model" + "github.com/pocketbase/dbx" ) type playerRepository struct { @@ -15,10 +15,10 @@ type playerRepository struct { sqlRestful } -func NewPlayerRepository(ctx context.Context, o orm.QueryExecutor) model.PlayerRepository { +func NewPlayerRepository(ctx context.Context, db dbx.Builder) model.PlayerRepository { r := &playerRepository{} r.ctx = ctx - r.ormer = o + r.db = db r.tableName = "player" r.filterMappings = map[string]filterFunc{ "name": containsFilter, diff --git a/persistence/playlist_repository.go b/persistence/playlist_repository.go index f2addc15..0cae4b2b 100644 --- a/persistence/playlist_repository.go +++ b/persistence/playlist_repository.go @@ -2,18 +2,18 @@ package persistence import ( "context" + "database/sql" "encoding/json" "errors" - "strings" "time" . "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/deluan/rest" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/criteria" "github.com/navidrome/navidrome/utils/slice" + "github.com/pocketbase/dbx" ) type playlistRepository struct { @@ -23,13 +23,30 @@ type playlistRepository struct { type dbPlaylist struct { model.Playlist `structs:",flatten"` - RawRules string `structs:"rules" orm:"column(rules)"` + Rules sql.NullString `structs:"-"` } -func NewPlaylistRepository(ctx context.Context, o orm.QueryExecutor) model.PlaylistRepository { +func (p *dbPlaylist) PostScan() error { + if p.Rules.String != "" { + return json.Unmarshal([]byte(p.Rules.String), &p.Playlist.Rules) + } + return nil +} + +func (p dbPlaylist) PostMapArgs(args map[string]any) error { + var err error + if p.Playlist.IsSmartPlaylist() { + args["rules"], err = json.Marshal(p.Playlist.Rules) + return err + } + delete(args, "rules") + return nil +} + +func NewPlaylistRepository(ctx context.Context, db dbx.Builder) model.PlaylistRepository { r := &playlistRepository{} r.ctx = ctx - r.ormer = o + r.db = db r.tableName = "playlist" r.filterMappings = map[string]filterFunc{ "q": playlistFilter, @@ -64,8 +81,8 @@ func (r *playlistRepository) userFilter() Sqlizer { } func (r *playlistRepository) CountAll(options ...model.QueryOptions) (int64, error) { - sql := Select().Where(r.userFilter()) - return r.count(sql, options...) + sq := Select().Where(r.userFilter()) + return r.count(sq, options...) } func (r *playlistRepository) Exists(id string) (bool, error) { @@ -88,13 +105,6 @@ func (r *playlistRepository) Delete(id string) error { func (r *playlistRepository) Put(p *model.Playlist) error { pls := dbPlaylist{Playlist: *p} - if p.IsSmartPlaylist() { - j, err := json.Marshal(p.Rules) - if err != nil { - return err - } - pls.RawRules = string(j) - } if pls.ID == "" { pls.CreatedAt = time.Now() } else { @@ -161,22 +171,7 @@ func (r *playlistRepository) findBy(sql Sqlizer) (*model.Playlist, error) { return nil, model.ErrNotFound } - return r.toModel(pls[0]) -} - -func (r *playlistRepository) toModel(pls dbPlaylist) (*model.Playlist, error) { - var err error - if strings.TrimSpace(pls.RawRules) != "" { - var c criteria.Criteria - err = json.Unmarshal([]byte(pls.RawRules), &c) - if err != nil { - return nil, err - } - pls.Playlist.Rules = &c - } else { - pls.Playlist.Rules = nil - } - return &pls.Playlist, err + return &pls[0].Playlist, nil } func (r *playlistRepository) GetAll(options ...model.QueryOptions) (model.Playlists, error) { @@ -188,11 +183,7 @@ func (r *playlistRepository) GetAll(options ...model.QueryOptions) (model.Playli } playlists := make(model.Playlists, len(res)) for i, p := range res { - pls, err := r.toModel(p) - if err != nil { - return nil, err - } - playlists[i] = *pls + playlists[i] = p.Playlist } return playlists, err } @@ -204,13 +195,14 @@ func (r *playlistRepository) selectPlaylist(options ...model.QueryOptions) Selec func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool { // Only refresh if it is a smart playlist and was not refreshed in the last 5 seconds - if !pls.IsSmartPlaylist() || time.Since(pls.EvaluatedAt) < 5*time.Second { + if !pls.IsSmartPlaylist() || (pls.EvaluatedAt != nil && time.Since(*pls.EvaluatedAt) < 5*time.Second) { return false } // Never refresh other users' playlists usr := loggedUser(r.ctx) if pls.OwnerID != usr.ID { + log.Trace(r.ctx, "Not refreshing smart playlist from other user", "playlist", pls.Name, "id", pls.ID) return false } @@ -221,20 +213,21 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool { del := Delete("playlist_tracks").Where(Eq{"playlist_id": pls.ID}) _, err := r.executeSQL(del) if err != nil { + log.Error(r.ctx, "Error deleting old smart playlist tracks", "playlist", pls.Name, "id", pls.ID, err) return false } // Re-populate playlist based on Smart Playlist criteria rules := *pls.Rules - sql := Select("row_number() over (order by "+rules.OrderBy()+") as id", "'"+pls.ID+"' as playlist_id", "media_file.id as media_file_id"). + sq := Select("row_number() over (order by "+rules.OrderBy()+") as id", "'"+pls.ID+"' as playlist_id", "media_file.id as media_file_id"). From("media_file").LeftJoin("annotation on (" + "annotation.item_id = media_file.id" + " AND annotation.item_type = 'media_file'" + " AND annotation.user_id = '" + userId(r.ctx) + "')"). LeftJoin("media_file_genres ag on media_file.id = ag.media_file_id"). LeftJoin("genre on ag.genre_id = genre.id").GroupBy("media_file.id") - sql = r.addCriteria(sql, rules) - insSql := Insert("playlist_tracks").Columns("id", "playlist_id", "media_file_id").Select(sql) + sq = r.addCriteria(sq, rules) + insSql := Insert("playlist_tracks").Columns("id", "playlist_id", "media_file_id").Select(sq) _, err = r.executeSQL(insSql) if err != nil { log.Error(r.ctx, "Error refreshing smart playlist tracks", "playlist", pls.Name, "id", pls.ID, err) @@ -262,7 +255,7 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool { } func (r *playlistRepository) addCriteria(sql SelectBuilder, c criteria.Criteria) SelectBuilder { - sql = sql.Where(c.ToSql()) + sql = sql.Where(c) if c.Limit > 0 { sql = sql.Limit(uint64(c.Limit)).Offset(uint64(c.Offset)) } @@ -318,7 +311,11 @@ func (r *playlistRepository) addTracks(playlistId string, startingPos int, media // RefreshStatus updates total playlist duration, size and count func (r *playlistRepository) refreshCounters(pls *model.Playlist) error { - statsSql := Select("sum(duration) as duration", "sum(size) as size", "count(*) as count"). + statsSql := Select( + "coalesce(sum(duration), 0) as duration", + "coalesce(sum(size), 0) as size", + "count(*) as count", + ). From("media_file"). Join("playlist_tracks f on f.media_file_id = media_file.id"). Where(Eq{"playlist_id": pls.ID}) @@ -347,7 +344,15 @@ func (r *playlistRepository) refreshCounters(pls *model.Playlist) error { func (r *playlistRepository) loadTracks(sel SelectBuilder, id string) (model.PlaylistTracks, error) { tracksQuery := sel. - Columns("starred", "starred_at", "play_count", "play_date", "rating", "f.*", "playlist_tracks.*"). + Columns( + "coalesce(starred, 0)", + "starred_at", + "coalesce(play_count, 0)", + "play_date", + "coalesce(rating, 0)", + "f.*", + "playlist_tracks.*", + ). LeftJoin("annotation on (" + "annotation.item_id = media_file_id" + " AND annotation.item_type = 'media_file'" + @@ -402,7 +407,7 @@ func (r *playlistRepository) Update(id string, entity interface{}, cols ...strin if !usr.IsAdmin && current.OwnerID != usr.ID { return rest.ErrPermissionDenied } - pls := entity.(*model.Playlist) + pls := dbPlaylist{Playlist: *entity.(*model.Playlist)} pls.ID = id pls.UpdatedAt = time.Now() _, err = r.put(id, pls, append(cols, "updatedAt")...) @@ -447,8 +452,8 @@ func (r *playlistRepository) removeOrphans() error { func (r *playlistRepository) renumber(id string) error { var ids []string - sql := Select("media_file_id").From("playlist_tracks").Where(Eq{"playlist_id": id}).OrderBy("id") - err := r.queryAll(sql, &ids) + sq := Select("media_file_id").From("playlist_tracks").Where(Eq{"playlist_id": id}).OrderBy("id") + err := r.queryAllSlice(sq, &ids) if err != nil { return err } diff --git a/persistence/playlist_repository_test.go b/persistence/playlist_repository_test.go index 01672c51..3721b32e 100644 --- a/persistence/playlist_repository_test.go +++ b/persistence/playlist_repository_test.go @@ -3,9 +3,9 @@ package persistence import ( "context" - "github.com/beego/beego/v2/client/orm" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/model/criteria" "github.com/navidrome/navidrome/model/request" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -17,7 +17,7 @@ var _ = Describe("PlaylistRepository", func() { BeforeEach(func() { ctx := log.NewContext(context.TODO()) ctx = request.WithUser(ctx, model.User{ID: "userid", UserName: "userid", IsAdmin: true}) - repo = NewPlaylistRepository(ctx, orm.NewOrm()) + repo = NewPlaylistRepository(ctx, getDBXBuilder()) }) Describe("Count", func() { @@ -56,7 +56,7 @@ var _ = Describe("PlaylistRepository", func() { }) It("returns all tracks", func() { pls, err := repo.GetWithTracks(plsBest.ID, true) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) Expect(pls.Name).To(Equal(plsBest.Name)) Expect(pls.Tracks).To(HaveLen(2)) Expect(pls.Tracks[0].ID).To(Equal("1")) @@ -109,4 +109,23 @@ var _ = Describe("PlaylistRepository", func() { Expect(all[1].ID).To(Equal(plsCool.ID)) }) }) + + Context("Smart Playlists", func() { + var rules *criteria.Criteria + BeforeEach(func() { + rules = &criteria.Criteria{ + Expression: criteria.All{ + criteria.Contains{"title": "love"}, + }, + } + }) + It("Put/Get", func() { + newPls := model.Playlist{Name: "Great!", OwnerID: "userid", Rules: rules} + Expect(repo.Put(&newPls)).To(Succeed()) + + savedPls, err := repo.Get(newPls.ID) + Expect(err).ToNot(HaveOccurred()) + Expect(savedPls.Rules).To(Equal(rules)) + }) + }) }) diff --git a/persistence/playlist_track_repository.go b/persistence/playlist_track_repository.go index 24715a17..beea26a4 100644 --- a/persistence/playlist_track_repository.go +++ b/persistence/playlist_track_repository.go @@ -1,6 +1,8 @@ package persistence import ( + "database/sql" + . "github.com/Masterminds/squirrel" "github.com/deluan/rest" "github.com/navidrome/navidrome/log" @@ -21,7 +23,7 @@ func (r *playlistRepository) Tracks(playlistId string, refreshSmartPlaylist bool p.playlistRepo = r p.playlistId = playlistId p.ctx = r.ctx - p.ormer = r.ormer + p.db = r.db p.tableName = "playlist_tracks" p.sortMappings = map[string]string{ "id": "playlist_tracks.id", @@ -47,7 +49,15 @@ func (r *playlistTrackRepository) Read(id string) (interface{}, error) { "annotation.item_id = media_file_id"+ " AND annotation.item_type = 'media_file'"+ " AND annotation.user_id = '"+userId(r.ctx)+"')"). - Columns("starred", "starred_at", "play_count", "play_date", "rating", "f.*", "playlist_tracks.*"). + Columns( + "coalesce(starred, 0)", + "coalesce(play_count, 0)", + "coalesce(rating, 0)", + "starred_at", + "play_date", + "f.*", + "playlist_tracks.*", + ). Join("media_file f on f.id = media_file_id"). Where(And{Eq{"playlist_id": r.playlistId}, Eq{"id": id}}) var trk model.PlaylistTrack @@ -77,7 +87,7 @@ func (r *playlistTrackRepository) GetAlbumIDs(options ...model.QueryOptions) ([] Join("media_file mf on mf.id = media_file_id"). Where(Eq{"playlist_id": r.playlistId}) var ids []string - err := r.queryAll(sql, &ids) + err := r.queryAllSlice(sql, &ids) if err != nil { return nil, err } @@ -112,20 +122,20 @@ func (r *playlistTrackRepository) Add(mediaFileIds []string) (int, error) { } // Get next pos (ID) in playlist - sql := r.newSelect().Columns("max(id) as max").Where(Eq{"playlist_id": r.playlistId}) - var max int - err := r.queryOne(sql, &max) + sq := r.newSelect().Columns("max(id) as max").Where(Eq{"playlist_id": r.playlistId}) + var res struct{ Max sql.NullInt32 } + err := r.queryOne(sq, &res) if err != nil { return 0, err } - return len(mediaFileIds), r.playlistRepo.addTracks(r.playlistId, max+1, mediaFileIds) + return len(mediaFileIds), r.playlistRepo.addTracks(r.playlistId, int(res.Max.Int32+1), mediaFileIds) } func (r *playlistTrackRepository) addMediaFileIds(cond Sqlizer) (int, error) { sq := Select("id").From("media_file").Where(cond).OrderBy("album_artist, album, release_date, disc_number, track_number") var ids []string - err := r.queryAll(sq, &ids) + err := r.queryAllSlice(sq, &ids) if err != nil { log.Error(r.ctx, "Error getting tracks to add to playlist", err) return 0, err @@ -156,7 +166,7 @@ func (r *playlistTrackRepository) AddDiscs(discs []model.DiscID) (int, error) { func (r *playlistTrackRepository) getTracks() ([]string, error) { all := r.newSelect().Columns("media_file_id").Where(Eq{"playlist_id": r.playlistId}).OrderBy("id") var ids []string - err := r.queryAll(all, &ids) + err := r.queryAllSlice(all, &ids) if err != nil { log.Error(r.ctx, "Error querying current tracks from playlist", "playlistId", r.playlistId, err) return nil, err diff --git a/persistence/playqueue_repository.go b/persistence/playqueue_repository.go index beda2a83..fa21f184 100644 --- a/persistence/playqueue_repository.go +++ b/persistence/playqueue_repository.go @@ -6,27 +6,27 @@ import ( "time" . "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/utils/slice" + "github.com/pocketbase/dbx" ) type playQueueRepository struct { sqlRepository } -func NewPlayQueueRepository(ctx context.Context, o orm.QueryExecutor) model.PlayQueueRepository { +func NewPlayQueueRepository(ctx context.Context, db dbx.Builder) model.PlayQueueRepository { r := &playQueueRepository{} r.ctx = ctx - r.ormer = o + r.db = db r.tableName = "playqueue" return r } type playQueue struct { - ID string `structs:"id" orm:"column(id)"` - UserID string `structs:"user_id" orm:"column(user_id)"` + ID string `structs:"id"` + UserID string `structs:"user_id"` Current string `structs:"current"` Position int64 `structs:"position"` ChangedBy string `structs:"changed_by"` @@ -116,7 +116,7 @@ func (r *playQueueRepository) loadTracks(tracks model.MediaFiles) model.MediaFil chunks := slice.BreakUp(ids, 50) // Query each chunk of media_file ids and store results in a map - mfRepo := NewMediaFileRepository(r.ctx, r.ormer) + mfRepo := NewMediaFileRepository(r.ctx, r.db) trackMap := map[string]model.MediaFile{} for i := range chunks { idsFilter := Eq{"media_file.id": chunks[i]} diff --git a/persistence/playqueue_repository_test.go b/persistence/playqueue_repository_test.go index e1103dfa..1d983694 100644 --- a/persistence/playqueue_repository_test.go +++ b/persistence/playqueue_repository_test.go @@ -5,7 +5,6 @@ import ( "time" "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/google/uuid" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" @@ -19,8 +18,8 @@ var _ = Describe("PlayQueueRepository", func() { BeforeEach(func() { ctx := log.NewContext(context.TODO()) - ctx = request.WithUser(ctx, model.User{ID: "user1", UserName: "user1", IsAdmin: true}) - repo = NewPlayQueueRepository(ctx, orm.NewOrm()) + ctx = request.WithUser(ctx, model.User{ID: "userid", UserName: "userid", IsAdmin: true}) + repo = NewPlayQueueRepository(ctx, getDBXBuilder()) }) Describe("PlayQueues", func() { @@ -32,24 +31,24 @@ var _ = Describe("PlayQueueRepository", func() { It("stores and retrieves the playqueue for the user", func() { By("Storing a playqueue for the user") - expected := aPlayQueue("user1", songDayInALife.ID, 123, songComeTogether, songDayInALife) - Expect(repo.Store(expected)).To(BeNil()) + expected := aPlayQueue("userid", songDayInALife.ID, 123, songComeTogether, songDayInALife) + Expect(repo.Store(expected)).To(Succeed()) - actual, err := repo.Retrieve("user1") - Expect(err).To(BeNil()) + actual, err := repo.Retrieve("userid") + Expect(err).ToNot(HaveOccurred()) AssertPlayQueue(expected, actual) By("Storing a new playqueue for the same user") - another := aPlayQueue("user1", songRadioactivity.ID, 321, songAntenna, songRadioactivity) - Expect(repo.Store(another)).To(BeNil()) + another := aPlayQueue("userid", songRadioactivity.ID, 321, songAntenna, songRadioactivity) + Expect(repo.Store(another)).To(Succeed()) - actual, err = repo.Retrieve("user1") - Expect(err).To(BeNil()) + actual, err = repo.Retrieve("userid") + Expect(err).ToNot(HaveOccurred()) AssertPlayQueue(another, actual) - Expect(countPlayQueues(repo, "user1")).To(Equal(1)) + Expect(countPlayQueues(repo, "userid")).To(Equal(1)) }) }) }) diff --git a/persistence/property_repository.go b/persistence/property_repository.go index d62cdec6..14f9051f 100644 --- a/persistence/property_repository.go +++ b/persistence/property_repository.go @@ -5,18 +5,18 @@ import ( "errors" . "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/navidrome/navidrome/model" + "github.com/pocketbase/dbx" ) type propertyRepository struct { sqlRepository } -func NewPropertyRepository(ctx context.Context, o orm.QueryExecutor) model.PropertyRepository { +func NewPropertyRepository(ctx context.Context, db dbx.Builder) model.PropertyRepository { r := &propertyRepository{} r.ctx = ctx - r.ormer = o + r.db = db r.tableName = "property" return r } diff --git a/persistence/property_repository_test.go b/persistence/property_repository_test.go index 4c5df5ea..172a7b0d 100644 --- a/persistence/property_repository_test.go +++ b/persistence/property_repository_test.go @@ -3,7 +3,6 @@ package persistence import ( "context" - "github.com/beego/beego/v2/client/orm" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" . "github.com/onsi/ginkgo/v2" @@ -14,7 +13,7 @@ var _ = Describe("Property Repository", func() { var pr model.PropertyRepository BeforeEach(func() { - pr = NewPropertyRepository(log.NewContext(context.TODO()), orm.NewOrm()) + pr = NewPropertyRepository(log.NewContext(context.TODO()), getDBXBuilder()) }) It("saves and restore a new property", func() { diff --git a/persistence/radio_repository.go b/persistence/radio_repository.go index 17e4d249..93727e4e 100644 --- a/persistence/radio_repository.go +++ b/persistence/radio_repository.go @@ -7,10 +7,10 @@ import ( "time" . "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/deluan/rest" "github.com/google/uuid" "github.com/navidrome/navidrome/model" + "github.com/pocketbase/dbx" ) type radioRepository struct { @@ -18,10 +18,10 @@ type radioRepository struct { sqlRestful } -func NewRadioRepository(ctx context.Context, o orm.QueryExecutor) model.RadioRepository { +func NewRadioRepository(ctx context.Context, db dbx.Builder) model.RadioRepository { r := &radioRepository{} r.ctx = ctx - r.ormer = o + r.db = db r.tableName = "radio" r.filterMappings = map[string]filterFunc{ "name": containsFilter, @@ -73,9 +73,9 @@ func (r *radioRepository) Put(radio *model.Radio) error { if radio.ID == "" { radio.CreatedAt = time.Now() radio.ID = strings.ReplaceAll(uuid.NewString(), "-", "") - values, _ = toSqlArgs(*radio) + values, _ = toSQLArgs(*radio) } else { - values, _ = toSqlArgs(*radio) + values, _ = toSQLArgs(*radio) update := Update(r.tableName).Where(Eq{"id": radio.ID}).SetMap(values) count, err := r.executeSQL(update) diff --git a/persistence/radio_repository_test.go b/persistence/radio_repository_test.go index b87b1fc1..2d819929 100644 --- a/persistence/radio_repository_test.go +++ b/persistence/radio_repository_test.go @@ -3,7 +3,6 @@ package persistence import ( "context" - "github.com/beego/beego/v2/client/orm" "github.com/deluan/rest" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" @@ -23,7 +22,7 @@ var _ = Describe("RadioRepository", func() { BeforeEach(func() { ctx := log.NewContext(context.TODO()) ctx = request.WithUser(ctx, model.User{ID: "userid", UserName: "userid", IsAdmin: true}) - repo = NewRadioRepository(ctx, orm.NewOrm()) + repo = NewRadioRepository(ctx, getDBXBuilder()) _ = repo.Put(&radioWithHomePage) }) @@ -120,7 +119,7 @@ var _ = Describe("RadioRepository", func() { BeforeEach(func() { ctx := log.NewContext(context.TODO()) ctx = request.WithUser(ctx, model.User{ID: "userid", UserName: "userid", IsAdmin: false}) - repo = NewRadioRepository(ctx, orm.NewOrm()) + repo = NewRadioRepository(ctx, getDBXBuilder()) }) Describe("Count", func() { diff --git a/persistence/scrobble_buffer_repository.go b/persistence/scrobble_buffer_repository.go index 3912f731..9407e68c 100644 --- a/persistence/scrobble_buffer_repository.go +++ b/persistence/scrobble_buffer_repository.go @@ -6,18 +6,18 @@ import ( "time" . "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/navidrome/navidrome/model" + "github.com/pocketbase/dbx" ) type scrobbleBufferRepository struct { sqlRepository } -func NewScrobbleBufferRepository(ctx context.Context, o orm.QueryExecutor) model.ScrobbleBufferRepository { +func NewScrobbleBufferRepository(ctx context.Context, db dbx.Builder) model.ScrobbleBufferRepository { r := &scrobbleBufferRepository{} r.ctx = ctx - r.ormer = o + r.db = db r.tableName = "scrobble_buffer" return r } @@ -31,7 +31,7 @@ func (r *scrobbleBufferRepository) UserIDs(service string) ([]string, error) { GroupBy("user_id"). OrderBy("count(*)") var userIds []string - err := r.queryAll(sql, &userIds) + err := r.queryAllSlice(sql, &userIds) return userIds, err } diff --git a/persistence/share_repository.go b/persistence/share_repository.go index ac586e6b..2547bcfa 100644 --- a/persistence/share_repository.go +++ b/persistence/share_repository.go @@ -8,11 +8,11 @@ import ( "time" . "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/deluan/rest" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" + "github.com/pocketbase/dbx" ) type shareRepository struct { @@ -20,10 +20,10 @@ type shareRepository struct { sqlRestful } -func NewShareRepository(ctx context.Context, o orm.QueryExecutor) model.ShareRepository { +func NewShareRepository(ctx context.Context, db dbx.Builder) model.ShareRepository { r := &shareRepository{} r.ctx = ctx - r.ormer = o + r.db = db r.tableName = "share" return r } @@ -79,27 +79,27 @@ func (r *shareRepository) loadMedia(share *model.Share) error { } switch share.ResourceType { case "artist": - albumRepo := NewAlbumRepository(r.ctx, r.ormer) + albumRepo := NewAlbumRepository(r.ctx, r.db) share.Albums, err = albumRepo.GetAll(model.QueryOptions{Filters: Eq{"album_artist_id": ids}, Sort: "artist"}) if err != nil { return err } - mfRepo := NewMediaFileRepository(r.ctx, r.ormer) + mfRepo := NewMediaFileRepository(r.ctx, r.db) share.Tracks, err = mfRepo.GetAll(model.QueryOptions{Filters: Eq{"album_artist_id": ids}, Sort: "artist"}) return err case "album": - albumRepo := NewAlbumRepository(r.ctx, r.ormer) + albumRepo := NewAlbumRepository(r.ctx, r.db) share.Albums, err = albumRepo.GetAll(model.QueryOptions{Filters: Eq{"id": ids}}) if err != nil { return err } - mfRepo := NewMediaFileRepository(r.ctx, r.ormer) + mfRepo := NewMediaFileRepository(r.ctx, r.db) share.Tracks, err = mfRepo.GetAll(model.QueryOptions{Filters: Eq{"album_id": ids}, Sort: "album"}) return err case "playlist": // Create a context with a fake admin user, to be able to access all playlists ctx := request.WithUser(r.ctx, model.User{IsAdmin: true}) - plsRepo := NewPlaylistRepository(ctx, r.ormer) + plsRepo := NewPlaylistRepository(ctx, r.db) tracks, err := plsRepo.Tracks(ids[0], true).GetAll(model.QueryOptions{Sort: "id"}) if err != nil { return err @@ -109,7 +109,7 @@ func (r *shareRepository) loadMedia(share *model.Share) error { } return nil case "media_file": - mfRepo := NewMediaFileRepository(r.ctx, r.ormer) + mfRepo := NewMediaFileRepository(r.ctx, r.db) tracks, err := mfRepo.GetAll(model.QueryOptions{Filters: Eq{"id": ids}}) share.Tracks = sortByIdPosition(tracks, ids) return err diff --git a/persistence/sql_annotations.go b/persistence/sql_annotations.go index 58c35965..7b31d07a 100644 --- a/persistence/sql_annotations.go +++ b/persistence/sql_annotations.go @@ -1,11 +1,11 @@ package persistence import ( + "database/sql" "errors" "time" . "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/google/uuid" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" @@ -42,7 +42,7 @@ func (r sqlRepository) annUpsert(values map[string]interface{}, itemIDs ...strin upd = upd.Set(f, v) } c, err := r.executeSQL(upd) - if c == 0 || errors.Is(err, orm.ErrNoRows) { + if c == 0 || errors.Is(err, sql.ErrNoRows) { for _, itemID := range itemIDs { values["ann_id"] = uuid.NewString() values["user_id"] = userId(r.ctx) @@ -73,7 +73,7 @@ func (r sqlRepository) IncPlayCount(itemID string, ts time.Time) error { Set("play_date", Expr("max(ifnull(play_date,''),?)", ts)) c, err := r.executeSQL(upd) - if c == 0 || errors.Is(err, orm.ErrNoRows) { + if c == 0 || errors.Is(err, sql.ErrNoRows) { values := map[string]interface{}{} values["ann_id"] = uuid.NewString() values["user_id"] = userId(r.ctx) diff --git a/persistence/sql_base_repository.go b/persistence/sql_base_repository.go index 10899817..b2c7421c 100644 --- a/persistence/sql_base_repository.go +++ b/persistence/sql_base_repository.go @@ -2,24 +2,25 @@ package persistence import ( "context" + "database/sql" "errors" "fmt" "strings" "time" . "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/google/uuid" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" + "github.com/pocketbase/dbx" ) type sqlRepository struct { ctx context.Context tableName string - ormer orm.QueryExecutor + db dbx.Builder sortMappings map[string]string } @@ -122,13 +123,13 @@ func (r sqlRepository) applyFilters(sq SelectBuilder, options ...model.QueryOpti } func (r sqlRepository) executeSQL(sq Sqlizer) (int64, error) { - query, args, err := sq.ToSql() + query, args, err := r.toSQL(sq) if err != nil { return 0, err } start := time.Now() var c int64 - res, err := r.ormer.Raw(query, args...).Exec() + res, err := r.db.NewQuery(query).Bind(args).Execute() if res != nil { c, _ = res.RowsAffected() } @@ -141,16 +142,31 @@ func (r sqlRepository) executeSQL(sq Sqlizer) (int64, error) { return res.RowsAffected() } +func (r sqlRepository) toSQL(sq Sqlizer) (string, dbx.Params, error) { + query, args, err := sq.ToSql() + if err != nil { + return "", nil, err + } + // Replace query placeholders with named params + params := dbx.Params{} + for i, arg := range args { + p := fmt.Sprintf("p%d", i) + query = strings.Replace(query, "?", "{:"+p+"}", 1) + params[p] = arg + } + return query, params, nil +} + // Note: Due to a bug in the QueryRow method, this function does not map any embedded structs (ex: annotations) // In this case, use the queryAll method and get the first item of the returned list func (r sqlRepository) queryOne(sq Sqlizer, response interface{}) error { - query, args, err := sq.ToSql() + query, args, err := r.toSQL(sq) if err != nil { return err } start := time.Now() - err = r.ormer.Raw(query, args...).QueryRow(response) - if errors.Is(err, orm.ErrNoRows) { + err = r.db.NewQuery(query).Bind(args).One(response) + if errors.Is(err, sql.ErrNoRows) { r.logSQL(query, args, nil, 0, start) return model.ErrNotFound } @@ -162,17 +178,33 @@ func (r sqlRepository) queryAll(sq SelectBuilder, response interface{}, options if len(options) > 0 && options[0].Offset > 0 { sq = r.optimizePagination(sq, options[0]) } - query, args, err := sq.ToSql() + query, args, err := r.toSQL(sq) if err != nil { return err } start := time.Now() - c, err := r.ormer.Raw(query, args...).QueryRows(response) - if errors.Is(err, orm.ErrNoRows) { - r.logSQL(query, args, nil, c, start) + err = r.db.NewQuery(query).Bind(args).All(response) + if errors.Is(err, sql.ErrNoRows) { + r.logSQL(query, args, nil, -1, start) return model.ErrNotFound } - r.logSQL(query, args, err, c, start) + r.logSQL(query, args, err, -1, start) + return err +} + +// queryAllSlice is a helper function to query a single column and return the result in a slice +func (r sqlRepository) queryAllSlice(sq SelectBuilder, response interface{}) error { + query, args, err := r.toSQL(sq) + if err != nil { + return err + } + start := time.Now() + err = r.db.NewQuery(query).Bind(args).Column(response) + if errors.Is(err, sql.ErrNoRows) { + r.logSQL(query, args, nil, -1, start) + return model.ErrNotFound + } + r.logSQL(query, args, err, -1, start) return err } @@ -207,7 +239,7 @@ func (r sqlRepository) count(countQuery SelectBuilder, options ...model.QueryOpt } func (r sqlRepository) put(id string, m interface{}, colsToUpdate ...string) (newId string, err error) { - values, _ := toSqlArgs(m) + values, _ := toSQLArgs(m) // If there's an ID, try to update first if id != "" { updateValues := map[string]interface{}{} @@ -246,28 +278,28 @@ func (r sqlRepository) put(id string, m interface{}, colsToUpdate ...string) (ne func (r sqlRepository) delete(cond Sqlizer) error { del := Delete(r.tableName).Where(cond) _, err := r.executeSQL(del) - if errors.Is(err, orm.ErrNoRows) { + if errors.Is(err, sql.ErrNoRows) { return model.ErrNotFound } return err } -func (r sqlRepository) logSQL(sql string, args []interface{}, err error, rowsAffected int64, start time.Time) { +func (r sqlRepository) logSQL(sql string, args dbx.Params, err error, rowsAffected int64, start time.Time) { elapsed := time.Since(start) - var fmtArgs []string - for i := range args { - var f string - switch a := args[i].(type) { - case string: - f = `'` + a + `'` - default: - f = fmt.Sprintf("%v", a) - } - fmtArgs = append(fmtArgs, f) - } + //var fmtArgs []string + //for name, val := range args { + // var f string + // switch a := args[val].(type) { + // case string: + // f = `'` + a + `'` + // default: + // f = fmt.Sprintf("%v", a) + // } + // fmtArgs = append(fmtArgs, f) + //} if err != nil { - log.Error(r.ctx, "SQL: `"+sql+"`", "args", `[`+strings.Join(fmtArgs, ",")+`]`, "rowsAffected", rowsAffected, "elapsedTime", elapsed, err) + log.Error(r.ctx, "SQL: `"+sql+"`", "args", args, "rowsAffected", rowsAffected, "elapsedTime", elapsed, err) } else { - log.Trace(r.ctx, "SQL: `"+sql+"`", "args", `[`+strings.Join(fmtArgs, ",")+`]`, "rowsAffected", rowsAffected, "elapsedTime", elapsed) + log.Trace(r.ctx, "SQL: `"+sql+"`", "args", args, "rowsAffected", rowsAffected, "elapsedTime", elapsed) } } diff --git a/persistence/sql_bookmarks.go b/persistence/sql_bookmarks.go index b5be079d..58c062c5 100644 --- a/persistence/sql_bookmarks.go +++ b/persistence/sql_bookmarks.go @@ -1,11 +1,11 @@ package persistence import ( + "database/sql" "errors" "time" . "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" @@ -19,7 +19,7 @@ func (r sqlRepository) withBookmark(sql SelectBuilder, idField string) SelectBui "bookmark.item_id = " + idField + " AND bookmark.item_type = '" + r.tableName + "'" + " AND bookmark.user_id = '" + userId(r.ctx) + "')"). - Columns("position as bookmark_position") + Columns("coalesce(position, 0) as bookmark_position") } func (r sqlRepository) bmkID(itemID ...string) And { @@ -45,7 +45,7 @@ func (r sqlRepository) bmkUpsert(itemID, comment string, position int64) error { if err == nil { log.Debug(r.ctx, "Updated bookmark", "id", itemID, "user", user.UserName, "position", position, "comment", comment) } - if c == 0 || errors.Is(err, orm.ErrNoRows) { + if c == 0 || errors.Is(err, sql.ErrNoRows) { values["user_id"] = user.ID values["item_type"] = r.tableName values["item_id"] = itemID @@ -82,8 +82,8 @@ func (r sqlRepository) DeleteBookmark(id string) error { } type bookmark struct { - UserID string `json:"user_id" orm:"column(user_id)"` - ItemID string `json:"item_id" orm:"column(item_id)"` + UserID string `json:"user_id"` + ItemID string `json:"item_id"` ItemType string `json:"item_type"` Comment string `json:"comment"` Position int64 `json:"position"` @@ -96,10 +96,10 @@ func (r sqlRepository) GetBookmarks() (model.Bookmarks, error) { user, _ := request.UserFrom(r.ctx) idField := r.tableName + ".id" - sql := r.newSelectWithAnnotation(idField).Columns("*") - sql = r.withBookmark(sql, idField).Where(NotEq{bookmarkTable + ".item_id": nil}) + sq := r.newSelectWithAnnotation(idField).Columns(r.tableName + ".*") + sq = r.withBookmark(sq, idField).Where(NotEq{bookmarkTable + ".item_id": nil}) var mfs model.MediaFiles - err := r.queryAll(sql, &mfs) + err := r.queryAll(sq, &mfs) if err != nil { log.Error(r.ctx, "Error getting mediafiles with bookmarks", "user", user.UserName, err) return nil, err @@ -117,9 +117,9 @@ func (r sqlRepository) GetBookmarks() (model.Bookmarks, error) { mfMap[mf.ID] = i } - sql = Select("*").From(bookmarkTable).Where(r.bmkID(ids...)) + sq = Select("*").From(bookmarkTable).Where(r.bmkID(ids...)) var bmks []bookmark - err = r.queryAll(sql, &bmks) + err = r.queryAll(sq, &bmks) if err != nil { log.Error(r.ctx, "Error getting bookmarks", "user", user.UserName, "ids", ids, err) return nil, err diff --git a/persistence/sql_bookmarks_test.go b/persistence/sql_bookmarks_test.go index f80c123c..c5447af2 100644 --- a/persistence/sql_bookmarks_test.go +++ b/persistence/sql_bookmarks_test.go @@ -3,7 +3,6 @@ package persistence import ( "context" - "github.com/beego/beego/v2/client/orm" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" @@ -16,8 +15,8 @@ var _ = Describe("sqlBookmarks", func() { BeforeEach(func() { ctx := log.NewContext(context.TODO()) - ctx = request.WithUser(ctx, model.User{ID: "user1"}) - mr = NewMediaFileRepository(ctx, orm.NewOrm()) + ctx = request.WithUser(ctx, model.User{ID: "userid"}) + mr = NewMediaFileRepository(ctx, getDBXBuilder()) }) Describe("Bookmarks", func() { @@ -30,7 +29,7 @@ var _ = Describe("sqlBookmarks", func() { Expect(mr.AddBookmark(songAntenna.ID, "this is a comment", 123)).To(BeNil()) bms, err := mr.GetBookmarks() - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) Expect(bms).To(HaveLen(1)) Expect(bms[0].Item.ID).To(Equal(songAntenna.ID)) @@ -46,7 +45,7 @@ var _ = Describe("sqlBookmarks", func() { Expect(mr.AddBookmark(songAntenna.ID, "another comment", 333)).To(BeNil()) bms, err = mr.GetBookmarks() - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) Expect(bms[0].Item.ID).To(Equal(songAntenna.ID)) Expect(bms[0].Comment).To(Equal("another comment")) @@ -57,16 +56,19 @@ var _ = Describe("sqlBookmarks", func() { By("Saving another bookmark") Expect(mr.AddBookmark(songComeTogether.ID, "one more comment", 444)).To(BeNil()) bms, err = mr.GetBookmarks() - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) Expect(bms).To(HaveLen(2)) By("Delete bookmark") - Expect(mr.DeleteBookmark(songAntenna.ID)) + Expect(mr.DeleteBookmark(songAntenna.ID)).To(Succeed()) bms, err = mr.GetBookmarks() - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) Expect(bms).To(HaveLen(1)) Expect(bms[0].Item.ID).To(Equal(songComeTogether.ID)) Expect(bms[0].Item.Title).To(Equal(songComeTogether.Title)) + + Expect(mr.DeleteBookmark(songComeTogether.ID)).To(Succeed()) + Expect(mr.GetBookmarks()).To(BeEmpty()) }) }) }) diff --git a/persistence/sql_search.go b/persistence/sql_search.go index bd429536..282455e8 100644 --- a/persistence/sql_search.go +++ b/persistence/sql_search.go @@ -21,7 +21,7 @@ func (r sqlRepository) doSearch(q string, offset, size int, results interface{}, return nil } - sq := r.newSelectWithAnnotation(r.tableName + ".id").Columns("*") + sq := r.newSelectWithAnnotation(r.tableName + ".id").Columns(r.tableName + ".*") filter := fullTextExpr(q) if filter != nil { sq = sq.Where(filter) diff --git a/persistence/transcoding_repository.go b/persistence/transcoding_repository.go index fc364fce..17cd0a01 100644 --- a/persistence/transcoding_repository.go +++ b/persistence/transcoding_repository.go @@ -5,9 +5,9 @@ import ( "errors" . "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/deluan/rest" "github.com/navidrome/navidrome/model" + "github.com/pocketbase/dbx" ) type transcodingRepository struct { @@ -15,10 +15,10 @@ type transcodingRepository struct { sqlRestful } -func NewTranscodingRepository(ctx context.Context, o orm.QueryExecutor) model.TranscodingRepository { +func NewTranscodingRepository(ctx context.Context, db dbx.Builder) model.TranscodingRepository { r := &transcodingRepository{} r.ctx = ctx - r.ormer = o + r.db = db r.tableName = "transcoding" return r } diff --git a/persistence/user_props_repository.go b/persistence/user_props_repository.go index 1fe9c7b9..9307385a 100644 --- a/persistence/user_props_repository.go +++ b/persistence/user_props_repository.go @@ -5,18 +5,18 @@ import ( "errors" . "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/navidrome/navidrome/model" + "github.com/pocketbase/dbx" ) type userPropsRepository struct { sqlRepository } -func NewUserPropsRepository(ctx context.Context, o orm.QueryExecutor) model.UserPropsRepository { +func NewUserPropsRepository(ctx context.Context, db dbx.Builder) model.UserPropsRepository { r := &userPropsRepository{} r.ctx = ctx - r.ormer = o + r.db = db r.tableName = "user_props" return r } diff --git a/persistence/user_repository.go b/persistence/user_repository.go index 0acbaae2..6019f891 100644 --- a/persistence/user_repository.go +++ b/persistence/user_repository.go @@ -10,7 +10,6 @@ import ( "time" . "github.com/Masterminds/squirrel" - "github.com/beego/beego/v2/client/orm" "github.com/deluan/rest" "github.com/google/uuid" "github.com/navidrome/navidrome/conf" @@ -18,6 +17,7 @@ import ( "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/utils" + "github.com/pocketbase/dbx" ) type userRepository struct { @@ -30,10 +30,10 @@ var ( encKey []byte ) -func NewUserRepository(ctx context.Context, o orm.QueryExecutor) model.UserRepository { +func NewUserRepository(ctx context.Context, db dbx.Builder) model.UserRepository { r := &userRepository{} r.ctx = ctx - r.ormer = o + r.db = db r.tableName = "user" once.Do(func() { _ = r.initPasswordEncryptionKey() @@ -67,7 +67,7 @@ func (r *userRepository) Put(u *model.User) error { if u.NewPassword != "" { _ = r.encryptPassword(u) } - values, _ := toSqlArgs(*u) + values, _ := toSQLArgs(*u) delete(values, "current_password") update := Update(r.tableName).Where(Eq{"id": u.ID}).SetMap(values) count, err := r.executeSQL(update) @@ -268,7 +268,7 @@ func (r *userRepository) initPasswordEncryptionKey() error { key := keyTo32Bytes(conf.Server.PasswordEncryptionKey) keySum := fmt.Sprintf("%x", sha256.Sum256(key)) - props := NewPropertyRepository(r.ctx, r.ormer) + props := NewPropertyRepository(r.ctx, r.db) savedKeySum, err := props.Get(consts.PasswordsEncryptedKey) // If passwords are already encrypted diff --git a/persistence/user_repository_test.go b/persistence/user_repository_test.go index d592b86d..762c2127 100644 --- a/persistence/user_repository_test.go +++ b/persistence/user_repository_test.go @@ -4,7 +4,6 @@ import ( "context" "errors" - "github.com/beego/beego/v2/client/orm" "github.com/deluan/rest" "github.com/google/uuid" "github.com/navidrome/navidrome/consts" @@ -19,7 +18,7 @@ var _ = Describe("UserRepository", func() { var repo model.UserRepository BeforeEach(func() { - repo = NewUserRepository(log.NewContext(context.TODO()), orm.NewOrm()) + repo = NewUserRepository(log.NewContext(context.TODO()), getDBXBuilder()) }) Describe("Put/Get/FindByUsername", func() { diff --git a/scanner/scanner_suite_test.go b/scanner/scanner_suite_test.go index 6c7f58c0..33a57c03 100644 --- a/scanner/scanner_suite_test.go +++ b/scanner/scanner_suite_test.go @@ -3,7 +3,6 @@ package scanner import ( "testing" - "github.com/beego/beego/v2/client/orm" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/db" "github.com/navidrome/navidrome/log" @@ -15,7 +14,6 @@ import ( func TestScanner(t *testing.T) { tests.Init(t, true) conf.Server.DbPath = "file::memory:?cache=shared" - _ = orm.RegisterDataBase("default", db.Driver, conf.Server.DbPath) db.Init() log.SetLevel(log.LevelFatal) RegisterFailHandler(Fail) diff --git a/server/auth.go b/server/auth.go index cc59fd19..6442e7e4 100644 --- a/server/auth.go +++ b/server/auth.go @@ -143,7 +143,7 @@ func createAdminUser(ctx context.Context, ds model.DataStore, username, password Email: "", NewPassword: password, IsAdmin: true, - LastLoginAt: now, + LastLoginAt: &now, } err := ds.User(ctx).Put(&initialUser) if err != nil { diff --git a/server/auth_test.go b/server/auth_test.go index dbaa79e1..51153585 100644 --- a/server/auth_test.go +++ b/server/auth_test.go @@ -9,6 +9,7 @@ import ( "net/http/httptest" "os" "strings" + "time" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/consts" @@ -31,9 +32,11 @@ var _ = Describe("Auth", func() { }) Describe("createAdmin", func() { + var createdAt time.Time BeforeEach(func() { req = httptest.NewRequest("POST", "/createAdmin", strings.NewReader(`{"username":"johndoe", "password":"secret"}`)) resp = httptest.NewRecorder() + createdAt = time.Now() createAdmin(ds)(resp, req) }) @@ -43,6 +46,7 @@ var _ = Describe("Auth", func() { Expect(err).To(BeNil()) Expect(u.Password).ToNot(BeEmpty()) Expect(u.IsAdmin).To(BeTrue()) + Expect(*u.LastLoginAt).To(BeTemporally(">=", createdAt, time.Second)) }) It("returns the expected payload", func() { diff --git a/server/subsonic/browsing.go b/server/subsonic/browsing.go index bc6bb246..8337315f 100644 --- a/server/subsonic/browsing.go +++ b/server/subsonic/browsing.go @@ -352,12 +352,12 @@ func (api *Router) buildArtistDirectory(ctx context.Context, artist *model.Artis dir.Name = artist.Name dir.PlayCount = artist.PlayCount if artist.PlayCount > 0 { - dir.Played = &artist.PlayDate + dir.Played = artist.PlayDate } dir.AlbumCount = int32(artist.AlbumCount) dir.UserRating = int32(artist.Rating) if artist.Starred { - dir.Starred = &artist.StarredAt + dir.Starred = artist.StarredAt } albums, err := api.ds.Album(ctx).GetAllWithoutGenres(filter.AlbumsByArtistID(artist.ID)) @@ -390,13 +390,13 @@ func (api *Router) buildAlbumDirectory(ctx context.Context, album *model.Album) dir.Parent = album.AlbumArtistID dir.PlayCount = album.PlayCount if album.PlayCount > 0 { - dir.Played = &album.PlayDate + dir.Played = album.PlayDate } dir.UserRating = int32(album.Rating) dir.SongCount = int32(album.SongCount) dir.CoverArt = album.CoverArtID().String() if album.Starred { - dir.Starred = &album.StarredAt + dir.Starred = album.StarredAt } mfs, err := api.ds.MediaFile(ctx).GetAll(filter.SongsByAlbum(album.ID)) @@ -419,7 +419,7 @@ func (api *Router) buildAlbum(ctx context.Context, album *model.Album, mfs model dir.Duration = int32(album.Duration) dir.PlayCount = album.PlayCount if album.PlayCount > 0 { - dir.Played = &album.PlayDate + dir.Played = album.PlayDate } dir.Year = int32(album.MaxYear) dir.Genre = album.Genre @@ -429,7 +429,7 @@ func (api *Router) buildAlbum(ctx context.Context, album *model.Album, mfs model dir.Created = &album.CreatedAt } if album.Starred { - dir.Starred = &album.StarredAt + dir.Starred = album.StarredAt } dir.MusicBrainzId = album.MbzAlbumID dir.IsCompilation = album.Compilation diff --git a/server/subsonic/helpers.go b/server/subsonic/helpers.go index 0f4ab4a6..ab06b3a0 100644 --- a/server/subsonic/helpers.go +++ b/server/subsonic/helpers.go @@ -97,7 +97,7 @@ func toArtist(r *http.Request, a model.Artist) responses.Artist { ArtistImageUrl: public.ImageURL(r, a.CoverArtID(), 600), } if a.Starred { - artist.Starred = &a.StarredAt + artist.Starred = a.StarredAt } return artist } @@ -114,7 +114,7 @@ func toArtistID3(r *http.Request, a model.Artist) responses.ArtistID3 { SortName: a.SortArtistName, } if a.Starred { - artist.Starred = &a.StarredAt + artist.Starred = a.StarredAt } return artist } @@ -174,10 +174,10 @@ func childFromMediaFile(ctx context.Context, mf model.MediaFile) responses.Child child.Type = "music" child.PlayCount = mf.PlayCount if mf.PlayCount > 0 { - child.Played = &mf.PlayDate + child.Played = mf.PlayDate } if mf.Starred { - child.Starred = &mf.StarredAt + child.Starred = mf.StarredAt } child.UserRating = int32(mf.Rating) @@ -239,11 +239,11 @@ func childFromAlbum(_ context.Context, al model.Album) responses.Child { child.Duration = int32(al.Duration) child.SongCount = int32(al.SongCount) if al.Starred { - child.Starred = &al.StarredAt + child.Starred = al.StarredAt } child.PlayCount = al.PlayCount if al.PlayCount > 0 { - child.Played = &al.PlayDate + child.Played = al.PlayDate } child.UserRating = int32(al.Rating) child.SortName = al.SortAlbumName diff --git a/server/subsonic/searching.go b/server/subsonic/searching.go index 8b3e276c..b5dfa583 100644 --- a/server/subsonic/searching.go +++ b/server/subsonic/searching.go @@ -110,7 +110,7 @@ func (api *Router) Search2(r *http.Request) (*responses.Subsonic, error) { ArtistImageUrl: public.ImageURL(r, artist.CoverArtID(), 600), } if artist.Starred { - searchResult2.Artist[i].Starred = &as[i].StarredAt + searchResult2.Artist[i].Starred = as[i].StarredAt } } searchResult2.Album = childrenFromAlbums(ctx, als) diff --git a/tests/mock_album_repo.go b/tests/mock_album_repo.go index 65ddfecd..2fa465dc 100644 --- a/tests/mock_album_repo.go +++ b/tests/mock_album_repo.go @@ -84,7 +84,7 @@ func (m *MockAlbumRepo) IncPlayCount(id string, timestamp time.Time) error { } if d, ok := m.data[id]; ok { d.PlayCount++ - d.PlayDate = timestamp + d.PlayDate = ×tamp return nil } return model.ErrNotFound diff --git a/tests/mock_artist_repo.go b/tests/mock_artist_repo.go index aa0a83f3..1501b393 100644 --- a/tests/mock_artist_repo.go +++ b/tests/mock_artist_repo.go @@ -67,7 +67,7 @@ func (m *MockArtistRepo) IncPlayCount(id string, timestamp time.Time) error { } if d, ok := m.data[id]; ok { d.PlayCount++ - d.PlayDate = timestamp + d.PlayDate = ×tamp return nil } return model.ErrNotFound diff --git a/tests/mock_mediafile_repo.go b/tests/mock_mediafile_repo.go index 03db44d0..63f1eda0 100644 --- a/tests/mock_mediafile_repo.go +++ b/tests/mock_mediafile_repo.go @@ -79,7 +79,7 @@ func (m *MockMediaFileRepo) IncPlayCount(id string, timestamp time.Time) error { } if d, ok := m.data[id]; ok { d.PlayCount++ - d.PlayDate = timestamp + d.PlayDate = ×tamp return nil } return model.ErrNotFound