Replace beego/orm with dbx (#2693)

* Start migration to dbx package

* Fix annotations and bookmarks bindings

* Fix tests

* Fix more tests

* Remove remaining references to beego/orm

* Add PostScanner/PostMapper interfaces

* Fix importing SmartPlaylists

* Renaming

* More renaming

* Fix artist DB mapping

* Fix playlist updates

* Remove bookmarks at the end of the test

* Remove remaining `orm` struct tags

* Fix user timestamps DB access

* Fix smart playlist evaluated_at DB access

* Fix search3
This commit is contained in:
Deluan Quintão 2023-12-09 13:52:17 -05:00 committed by GitHub
parent 7074455e0e
commit 0ca0d5da22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 461 additions and 376 deletions

3
.gitignore vendored
View File

@ -24,4 +24,5 @@ navidrome.db-wal
tags
.gitinfo
docker-compose.yml
!contrib/docker-compose.yml
!contrib/docker-compose.yml
test-123.db

View File

@ -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

5
go.mod
View File

@ -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

13
go.sum
View File

@ -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=

View File

@ -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"`

View File

@ -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 {

View File

@ -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"`
}

View File

@ -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:"-"`

View File

@ -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)

View File

@ -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"`

View File

@ -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
}

View File

@ -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"`

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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"`

View File

@ -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:"-"`

View File

@ -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",

View File

@ -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() {

View File

@ -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 {

View File

@ -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),

View File

@ -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{}

View File

@ -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}))
})

View File

@ -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

View File

@ -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"))

View File

@ -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
}

View File

@ -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() {

View File

@ -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}
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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,

View File

@ -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
}

View File

@ -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))
})
})
})

View File

@ -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

View File

@ -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]}

View File

@ -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))
})
})
})

View File

@ -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
}

View File

@ -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() {

View File

@ -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)

View File

@ -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() {

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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

View File

@ -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())
})
})
})

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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() {

View File

@ -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)

View File

@ -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 {

View File

@ -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() {

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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 = &timestamp
return nil
}
return model.ErrNotFound

View File

@ -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 = &timestamp
return nil
}
return model.ErrNotFound

View File

@ -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 = &timestamp
return nil
}
return model.ErrNotFound