mirror of
https://github.com/navidrome/navidrome.git
synced 2026-01-04 12:58:11 -05:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1278863416 | ||
|
|
cffae3a7d6 | ||
|
|
0d2911daf9 | ||
|
|
3c54b776d6 | ||
|
|
34127805ce | ||
|
|
ac4aa1ebe2 | ||
|
|
b7d7251cf4 | ||
|
|
7bf110f22f | ||
|
|
3b651a3728 | ||
|
|
13390c2edb | ||
|
|
221d320ccf |
@@ -84,3 +84,4 @@ changelog:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- '^ci:'
|
||||
|
||||
@@ -63,7 +63,7 @@ Navidrome is actively being tested with:
|
||||
| `star` | |
|
||||
| `unstar` | |
|
||||
| `setRating` | Doesn't work with artists |
|
||||
| `scrobble` | No Last.FM support yet. It is used to update play count, last played, skip count and last skipped |
|
||||
| `scrobble` | No Last.FM support yet. It is used to update play count and last played |
|
||||
| ||
|
||||
| _USER MANAGEMENT_ ||
|
||||
| `getUser` | Hardcoded all roles, ignores `username` parameter|
|
||||
|
||||
@@ -34,8 +34,10 @@ COPY --from=jsbuilder /src/build/* /src/ui/build/
|
||||
COPY --from=jsbuilder /src/build/static/css/* /src/ui/build/static/css/
|
||||
COPY --from=jsbuilder /src/build/static/js/* /src/ui/build/static/js/
|
||||
RUN rm -rf /src/build/css /src/build/js
|
||||
RUN go-bindata -fs -prefix ui/build -tags embed -nocompress -pkg assets -o assets/embedded_gen.go ui/build/...\
|
||||
&& go build -ldflags="-X main.gitSha=${SOURCE_COMMIT} -X main.gitTag=${SOURCE_BRANCH}" -tags=embed
|
||||
RUN GIT_SHA=$(git rev-parse --short HEAD) && \
|
||||
GIT_TAG=$(git describe --tags --abbrev=0 2> /dev/null) && \
|
||||
go-bindata -fs -prefix ui/build -tags embed -nocompress -pkg assets -o assets/embedded_gen.go ui/build/... && \
|
||||
go build -ldflags="-X main.gitSha=${GIT_SHA} -X main.gitTag=${GIT_TAG}" -tags=embed
|
||||
|
||||
#####################################################
|
||||
### Build Final Image
|
||||
|
||||
16
README.md
16
README.md
@@ -1,14 +1,17 @@
|
||||
# Navidrome Music Streamer
|
||||
|
||||
[](https://github.com/deluan/navidrome/actions)
|
||||
[](https://github.com/deluan/navidrome/actions)
|
||||
[](https://github.com/deluan/navidrome/releases)
|
||||
[](https://hub.docker.com/r/deluan/navidrome)
|
||||
|
||||
Navidrome is an open source web-based music collection server and streamer. It gives you freedom to listen to your
|
||||
music collection from any browser or mobile device.
|
||||
|
||||
This is _alpha quality_ software. Expect some changes in the feature set and the way it works.
|
||||
This is a fully functional _alpha quality_ software. Expect some changes in the feature set and the way it works.
|
||||
|
||||
__Any feedback is welcome!__ If you need/want a new feature, find a bug or think of any way to improve Navidrome,
|
||||
please fill a [GitHub issue](https://github.com/deluan/navidrome/issues)
|
||||
please fill a [GitHub issue](https://github.com/deluan/navidrome/issues) or join the chat in our [Discord server](https://discord.gg/xh7j7yF)
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
@@ -32,7 +35,7 @@ on a frequent basis. Some upcoming features planned:
|
||||
- Transcoding/Downsampling on-the-fly
|
||||
- Last.FM integration
|
||||
- Integrated music player
|
||||
- Pre-build binaries for all platforms, including Raspberry Pi
|
||||
- Pre-build binaries for Raspberry Pi
|
||||
- Smart/dynamic playlists (similar to iTunes)
|
||||
- Jukebox mode
|
||||
- Sharing links to albums/songs/playlists
|
||||
@@ -54,7 +57,7 @@ If you have any issues with these binaries, or need a binary for a different pla
|
||||
|
||||
### Docker
|
||||
|
||||
Docker images are available. Example of usage:
|
||||
[Docker images](https://hub.docker.com/r/deluan/navidrome) are available. They include everything needed to run Navidrome. Example of usage:
|
||||
|
||||
```yaml
|
||||
# This is just an example. Customize it to your needs.
|
||||
@@ -102,6 +105,8 @@ The server should start listening for requests on the default port __4533__
|
||||
After starting Navidrome for the first time, go to http://localhost:4533. It will ask you to create your first admin
|
||||
user.
|
||||
|
||||
For more options, run `navidrome --help`
|
||||
|
||||
## Screenshots
|
||||
|
||||
<p align="center">
|
||||
@@ -114,7 +119,6 @@ user.
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
## Subsonic API Version Compatibility
|
||||
|
||||
Check the up to date [compatibility table](https://github.com/deluan/navidrome/blob/master/API_COMPATIBILITY.md)
|
||||
|
||||
@@ -84,6 +84,9 @@ func LoadFromFile(confFile string, skipFlags ...bool) {
|
||||
if Server.DbPath == "" {
|
||||
Server.DbPath = filepath.Join(Server.DataFolder, "navidrome.db")
|
||||
}
|
||||
if os.Getenv("PORT") != "" {
|
||||
Server.Port = os.Getenv("PORT")
|
||||
}
|
||||
log.SerLevelString(Server.LogLevel)
|
||||
log.Trace("Loaded configuration", "file", confFile, "config", fmt.Sprintf("%#v", Server))
|
||||
}
|
||||
|
||||
@@ -32,11 +32,11 @@ type browser struct {
|
||||
}
|
||||
|
||||
func (b *browser) MediaFolders(ctx context.Context) (model.MediaFolders, error) {
|
||||
return b.ds.MediaFolder().GetAll()
|
||||
return b.ds.MediaFolder(ctx).GetAll()
|
||||
}
|
||||
|
||||
func (b *browser) Indexes(ctx context.Context, ifModifiedSince time.Time) (model.ArtistIndexes, time.Time, error) {
|
||||
l, err := b.ds.Property().DefaultGet(model.PropLastScan, "-1")
|
||||
l, err := b.ds.Property(ctx).DefaultGet(model.PropLastScan, "-1")
|
||||
ms, _ := strconv.ParseInt(l, 10, 64)
|
||||
lastModified := utils.ToTime(ms)
|
||||
|
||||
@@ -45,7 +45,7 @@ func (b *browser) Indexes(ctx context.Context, ifModifiedSince time.Time) (model
|
||||
}
|
||||
|
||||
if lastModified.After(ifModifiedSince) {
|
||||
indexes, err := b.ds.Artist().GetIndex()
|
||||
indexes, err := b.ds.Artist(ctx).GetIndex()
|
||||
return indexes, lastModified, err
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ type DirectoryInfo struct {
|
||||
}
|
||||
|
||||
func (b *browser) Artist(ctx context.Context, id string) (*DirectoryInfo, error) {
|
||||
a, albums, err := b.retrieveArtist(id)
|
||||
a, albums, err := b.retrieveArtist(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -81,12 +81,12 @@ func (b *browser) Artist(ctx context.Context, id string) (*DirectoryInfo, error)
|
||||
for _, al := range albums {
|
||||
albumIds = append(albumIds, al.ID)
|
||||
}
|
||||
annMap, err := b.ds.Annotation().GetMap(getUserID(ctx), model.AlbumItemType, albumIds)
|
||||
annMap, err := b.ds.Annotation(ctx).GetMap(getUserID(ctx), model.AlbumItemType, albumIds)
|
||||
return b.buildArtistDir(a, albums, annMap), nil
|
||||
}
|
||||
|
||||
func (b *browser) Album(ctx context.Context, id string) (*DirectoryInfo, error) {
|
||||
al, tracks, err := b.retrieveAlbum(id)
|
||||
al, tracks, err := b.retrieveAlbum(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -97,11 +97,11 @@ func (b *browser) Album(ctx context.Context, id string) (*DirectoryInfo, error)
|
||||
}
|
||||
|
||||
userID := getUserID(ctx)
|
||||
trackAnnMap, err := b.ds.Annotation().GetMap(userID, model.MediaItemType, mfIds)
|
||||
trackAnnMap, err := b.ds.Annotation(ctx).GetMap(userID, model.MediaItemType, mfIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ann, err := b.ds.Annotation().Get(userID, model.AlbumItemType, al.ID)
|
||||
ann, err := b.ds.Annotation(ctx).Get(userID, model.AlbumItemType, al.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -121,13 +121,13 @@ func (b *browser) Directory(ctx context.Context, id string) (*DirectoryInfo, err
|
||||
}
|
||||
|
||||
func (b *browser) GetSong(ctx context.Context, id string) (*Entry, error) {
|
||||
mf, err := b.ds.MediaFile().Get(id)
|
||||
mf, err := b.ds.MediaFile(ctx).Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userId := getUserID(ctx)
|
||||
ann, err := b.ds.Annotation().Get(userId, model.MediaItemType, id)
|
||||
ann, err := b.ds.Annotation(ctx).Get(userId, model.MediaItemType, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -137,7 +137,7 @@ func (b *browser) GetSong(ctx context.Context, id string) (*Entry, error) {
|
||||
}
|
||||
|
||||
func (b *browser) GetGenres(ctx context.Context) (model.Genres, error) {
|
||||
genres, err := b.ds.Genre().GetAll()
|
||||
genres, err := b.ds.Genre(ctx).GetAll()
|
||||
for i, g := range genres {
|
||||
if strings.TrimSpace(g.Name) == "" {
|
||||
genres[i].Name = "<Empty>"
|
||||
@@ -195,7 +195,7 @@ func (b *browser) buildAlbumDir(al *model.Album, albumAnn *model.Annotation, tra
|
||||
}
|
||||
|
||||
func (b *browser) isArtist(ctx context.Context, id string) bool {
|
||||
found, err := b.ds.Artist().Exists(id)
|
||||
found, err := b.ds.Artist(ctx).Exists(id)
|
||||
if err != nil {
|
||||
log.Debug(ctx, "Error searching for Artist", "id", id, err)
|
||||
return false
|
||||
@@ -204,7 +204,7 @@ func (b *browser) isArtist(ctx context.Context, id string) bool {
|
||||
}
|
||||
|
||||
func (b *browser) isAlbum(ctx context.Context, id string) bool {
|
||||
found, err := b.ds.Album().Exists(id)
|
||||
found, err := b.ds.Album(ctx).Exists(id)
|
||||
if err != nil {
|
||||
log.Debug(ctx, "Error searching for Album", "id", id, err)
|
||||
return false
|
||||
@@ -212,27 +212,27 @@ func (b *browser) isAlbum(ctx context.Context, id string) bool {
|
||||
return found
|
||||
}
|
||||
|
||||
func (b *browser) retrieveArtist(id string) (a *model.Artist, as model.Albums, err error) {
|
||||
a, err = b.ds.Artist().Get(id)
|
||||
func (b *browser) retrieveArtist(ctx context.Context, id string) (a *model.Artist, as model.Albums, err error) {
|
||||
a, err = b.ds.Artist(ctx).Get(id)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading Artist %s from DB: %v", id, err)
|
||||
return
|
||||
}
|
||||
|
||||
if as, err = b.ds.Album().FindByArtist(id); err != nil {
|
||||
if as, err = b.ds.Album(ctx).FindByArtist(id); err != nil {
|
||||
err = fmt.Errorf("Error reading %s's albums from DB: %v", a.Name, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *browser) retrieveAlbum(id string) (al *model.Album, mfs model.MediaFiles, err error) {
|
||||
al, err = b.ds.Album().Get(id)
|
||||
func (b *browser) retrieveAlbum(ctx context.Context, id string) (al *model.Album, mfs model.MediaFiles, err error) {
|
||||
al, err = b.ds.Album(ctx).Get(id)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading Album %s from DB: %v", id, err)
|
||||
return
|
||||
}
|
||||
|
||||
if mfs, err = b.ds.MediaFile().FindByAlbum(id); err != nil {
|
||||
if mfs, err = b.ds.MediaFile(ctx).FindByAlbum(id); err != nil {
|
||||
err = fmt.Errorf("Error reading %s's tracks from DB: %v", al.Name, err)
|
||||
}
|
||||
return
|
||||
|
||||
@@ -31,17 +31,17 @@ func NewCover(ds model.DataStore) Cover {
|
||||
return &cover{ds}
|
||||
}
|
||||
|
||||
func (c *cover) getCoverPath(id string) (string, error) {
|
||||
func (c *cover) getCoverPath(ctx context.Context, id string) (string, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(id, "al-"):
|
||||
id = id[3:]
|
||||
al, err := c.ds.Album().Get(id)
|
||||
al, err := c.ds.Album(ctx).Get(id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return al.CoverArtPath, nil
|
||||
default:
|
||||
mf, err := c.ds.MediaFile().Get(id)
|
||||
mf, err := c.ds.MediaFile(ctx).Get(id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func (c *cover) getCoverPath(id string) (string, error) {
|
||||
}
|
||||
|
||||
func (c *cover) Get(ctx context.Context, id string, size int, out io.Writer) error {
|
||||
path, err := c.getCoverPath(id)
|
||||
path, err := c.getCoverPath(ctx, id)
|
||||
if err != nil && err != model.ErrNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ func TestCover(t *testing.T) {
|
||||
Init(t, false)
|
||||
|
||||
ds := &persistence.MockDataStore{}
|
||||
mockMediaFileRepo := ds.MediaFile().(*persistence.MockMediaFile)
|
||||
mockAlbumRepo := ds.Album().(*persistence.MockAlbum)
|
||||
mockMediaFileRepo := ds.MediaFile(nil).(*persistence.MockMediaFile)
|
||||
mockAlbumRepo := ds.Album(nil).(*persistence.MockAlbum)
|
||||
|
||||
cover := engine.NewCover(ds)
|
||||
out := new(bytes.Buffer)
|
||||
|
||||
@@ -31,7 +31,7 @@ type listGenerator struct {
|
||||
}
|
||||
|
||||
func (g *listGenerator) query(ctx context.Context, qo model.QueryOptions) (Entries, error) {
|
||||
albums, err := g.ds.Album().GetAll(qo)
|
||||
albums, err := g.ds.Album(ctx).GetAll(qo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -39,7 +39,7 @@ func (g *listGenerator) query(ctx context.Context, qo model.QueryOptions) (Entri
|
||||
for i, al := range albums {
|
||||
albumIds[i] = al.ID
|
||||
}
|
||||
annMap, err := g.ds.Annotation().GetMap(getUserID(ctx), model.AlbumItemType, albumIds)
|
||||
annMap, err := g.ds.Annotation(ctx).GetMap(getUserID(ctx), model.AlbumItemType, albumIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -47,7 +47,7 @@ func (g *listGenerator) query(ctx context.Context, qo model.QueryOptions) (Entri
|
||||
}
|
||||
|
||||
func (g *listGenerator) queryByAnnotation(ctx context.Context, qo model.QueryOptions) (Entries, error) {
|
||||
annotations, err := g.ds.Annotation().GetAll(getUserID(ctx), model.AlbumItemType, qo)
|
||||
annotations, err := g.ds.Annotation(ctx).GetAll(getUserID(ctx), model.AlbumItemType, qo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func (g *listGenerator) queryByAnnotation(ctx context.Context, qo model.QueryOpt
|
||||
albumIds[i] = ann.ItemID
|
||||
}
|
||||
|
||||
albumMap, err := g.ds.Album().GetMap(albumIds)
|
||||
albumMap, err := g.ds.Album(ctx).GetMap(albumIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -103,7 +103,7 @@ func (g *listGenerator) GetByArtist(ctx context.Context, offset int, size int) (
|
||||
}
|
||||
|
||||
func (g *listGenerator) GetRandom(ctx context.Context, offset int, size int) (Entries, error) {
|
||||
albums, err := g.ds.Album().GetRandom(model.QueryOptions{Max: size, Offset: offset})
|
||||
albums, err := g.ds.Album(ctx).GetRandom(model.QueryOptions{Max: size, Offset: offset})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -120,7 +120,7 @@ func (g *listGenerator) getAnnotationsForAlbums(ctx context.Context, albums mode
|
||||
for i, al := range albums {
|
||||
albumIds[i] = al.ID
|
||||
}
|
||||
return g.ds.Annotation().GetMap(getUserID(ctx), model.AlbumItemType, albumIds)
|
||||
return g.ds.Annotation(ctx).GetMap(getUserID(ctx), model.AlbumItemType, albumIds)
|
||||
}
|
||||
|
||||
func (g *listGenerator) GetRandomSongs(ctx context.Context, size int, genre string) (Entries, error) {
|
||||
@@ -128,14 +128,14 @@ func (g *listGenerator) GetRandomSongs(ctx context.Context, size int, genre stri
|
||||
if genre != "" {
|
||||
options.Filters = map[string]interface{}{"genre": genre}
|
||||
}
|
||||
mediaFiles, err := g.ds.MediaFile().GetRandom(options)
|
||||
mediaFiles, err := g.ds.MediaFile(ctx).GetRandom(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := make(Entries, len(mediaFiles))
|
||||
for i, mf := range mediaFiles {
|
||||
ann, err := g.ds.Annotation().Get(getUserID(ctx), model.MediaItemType, mf.ID)
|
||||
ann, err := g.ds.Annotation(ctx).Get(getUserID(ctx), model.MediaItemType, mf.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func (g *listGenerator) GetRandomSongs(ctx context.Context, size int, genre stri
|
||||
|
||||
func (g *listGenerator) GetStarred(ctx context.Context, offset int, size int) (Entries, error) {
|
||||
qo := model.QueryOptions{Offset: offset, Max: size, Sort: "starred_at", Order: "desc"}
|
||||
albums, err := g.ds.Album().GetStarred(getUserID(ctx), qo)
|
||||
albums, err := g.ds.Album(ctx).GetStarred(getUserID(ctx), qo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -161,17 +161,17 @@ func (g *listGenerator) GetStarred(ctx context.Context, offset int, size int) (E
|
||||
func (g *listGenerator) GetAllStarred(ctx context.Context) (artists Entries, albums Entries, mediaFiles Entries, err error) {
|
||||
options := model.QueryOptions{Sort: "starred_at", Order: "desc"}
|
||||
|
||||
ars, err := g.ds.Artist().GetStarred(getUserID(ctx), options)
|
||||
ars, err := g.ds.Artist(ctx).GetStarred(getUserID(ctx), options)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
als, err := g.ds.Album().GetStarred(getUserID(ctx), options)
|
||||
als, err := g.ds.Album(ctx).GetStarred(getUserID(ctx), options)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
mfs, err := g.ds.MediaFile().GetStarred(getUserID(ctx), options)
|
||||
mfs, err := g.ds.MediaFile(ctx).GetStarred(getUserID(ctx), options)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
@@ -180,7 +180,7 @@ func (g *listGenerator) GetAllStarred(ctx context.Context) (artists Entries, alb
|
||||
for _, mf := range mfs {
|
||||
mfIds = append(mfIds, mf.ID)
|
||||
}
|
||||
trackAnnMap, err := g.ds.Annotation().GetMap(getUserID(ctx), model.MediaItemType, mfIds)
|
||||
trackAnnMap, err := g.ds.Annotation(ctx).GetMap(getUserID(ctx), model.MediaItemType, mfIds)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
@@ -194,7 +194,7 @@ func (g *listGenerator) GetAllStarred(ctx context.Context) (artists Entries, alb
|
||||
for _, ar := range ars {
|
||||
artistIds = append(artistIds, ar.ID)
|
||||
}
|
||||
artistAnnMap, err := g.ds.Annotation().GetMap(getUserID(ctx), model.MediaItemType, artistIds)
|
||||
artistAnnMap, err := g.ds.Annotation(ctx).GetMap(getUserID(ctx), model.MediaItemType, artistIds)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
@@ -213,11 +213,11 @@ func (g *listGenerator) GetNowPlaying(ctx context.Context) (Entries, error) {
|
||||
}
|
||||
entries := make(Entries, len(npInfo))
|
||||
for i, np := range npInfo {
|
||||
mf, err := g.ds.MediaFile().Get(np.TrackID)
|
||||
mf, err := g.ds.MediaFile(ctx).Get(np.TrackID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ann, err := g.ds.Annotation().Get(getUserID(ctx), model.MediaItemType, mf.ID)
|
||||
ann, err := g.ds.Annotation(ctx).Get(getUserID(ctx), model.MediaItemType, mf.ID)
|
||||
entries[i] = FromMediaFile(mf, ann)
|
||||
entries[i].UserName = np.Username
|
||||
entries[i].MinutesAgo = int(time.Now().Sub(np.Start).Minutes())
|
||||
|
||||
@@ -30,7 +30,7 @@ func (p *playlists) Create(ctx context.Context, playlistId, name string, ids []s
|
||||
var err error
|
||||
// If playlistID is present, override tracks
|
||||
if playlistId != "" {
|
||||
pls, err = p.ds.Playlist().Get(playlistId)
|
||||
pls, err = p.ds.Playlist(ctx).Get(playlistId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func (p *playlists) Create(ctx context.Context, playlistId, name string, ids []s
|
||||
pls.Tracks = append(pls.Tracks, model.MediaFile{ID: id})
|
||||
}
|
||||
|
||||
return p.ds.Playlist().Put(pls)
|
||||
return p.ds.Playlist(ctx).Put(pls)
|
||||
}
|
||||
|
||||
func (p *playlists) getUser(ctx context.Context) string {
|
||||
@@ -61,7 +61,7 @@ func (p *playlists) getUser(ctx context.Context) string {
|
||||
}
|
||||
|
||||
func (p *playlists) Delete(ctx context.Context, playlistId string) error {
|
||||
pls, err := p.ds.Playlist().Get(playlistId)
|
||||
pls, err := p.ds.Playlist(ctx).Get(playlistId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -70,11 +70,11 @@ func (p *playlists) Delete(ctx context.Context, playlistId string) error {
|
||||
if owner != pls.Owner {
|
||||
return model.ErrNotAuthorized
|
||||
}
|
||||
return p.ds.Playlist().Delete(playlistId)
|
||||
return p.ds.Playlist(nil).Delete(playlistId)
|
||||
}
|
||||
|
||||
func (p *playlists) Update(ctx context.Context, playlistId string, name *string, idsToAdd []string, idxToRemove []int) error {
|
||||
pls, err := p.ds.Playlist().Get(playlistId)
|
||||
pls, err := p.ds.Playlist(ctx).Get(playlistId)
|
||||
|
||||
owner := p.getUser(ctx)
|
||||
if owner != pls.Owner {
|
||||
@@ -100,11 +100,11 @@ func (p *playlists) Update(ctx context.Context, playlistId string, name *string,
|
||||
}
|
||||
pls.Tracks = newTracks
|
||||
|
||||
return p.ds.Playlist().Put(pls)
|
||||
return p.ds.Playlist(ctx).Put(pls)
|
||||
}
|
||||
|
||||
func (p *playlists) GetAll(ctx context.Context) (model.Playlists, error) {
|
||||
return p.ds.Playlist().GetAll(model.QueryOptions{})
|
||||
return p.ds.Playlist(ctx).GetAll(model.QueryOptions{})
|
||||
}
|
||||
|
||||
type PlaylistInfo struct {
|
||||
@@ -119,7 +119,7 @@ type PlaylistInfo struct {
|
||||
}
|
||||
|
||||
func (p *playlists) Get(ctx context.Context, id string) (*PlaylistInfo, error) {
|
||||
pl, err := p.ds.Playlist().GetWithTracks(id)
|
||||
pl, err := p.ds.Playlist(ctx).GetWithTracks(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -141,7 +141,7 @@ func (p *playlists) Get(ctx context.Context, id string) (*PlaylistInfo, error) {
|
||||
mfIds = append(mfIds, mf.ID)
|
||||
}
|
||||
|
||||
annMap, err := p.ds.Annotation().GetMap(getUserID(ctx), model.MediaItemType, mfIds)
|
||||
annMap, err := p.ds.Annotation(ctx).GetMap(getUserID(ctx), model.MediaItemType, mfIds)
|
||||
|
||||
for i, mf := range pl.Tracks {
|
||||
ann := annMap[mf.ID]
|
||||
|
||||
@@ -21,14 +21,14 @@ type ratings struct {
|
||||
}
|
||||
|
||||
func (r ratings) SetRating(ctx context.Context, id string, rating int) error {
|
||||
exist, err := r.ds.Album().Exists(id)
|
||||
exist, err := r.ds.Album(ctx).Exists(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist {
|
||||
return r.ds.Annotation().SetRating(rating, getUserID(ctx), model.AlbumItemType, id)
|
||||
return r.ds.Annotation(ctx).SetRating(rating, getUserID(ctx), model.AlbumItemType, id)
|
||||
}
|
||||
return r.ds.Annotation().SetRating(rating, getUserID(ctx), model.MediaItemType, id)
|
||||
return r.ds.Annotation(ctx).SetRating(rating, getUserID(ctx), model.MediaItemType, id)
|
||||
}
|
||||
|
||||
func (r ratings) SetStar(ctx context.Context, star bool, ids ...string) error {
|
||||
@@ -40,29 +40,29 @@ func (r ratings) SetStar(ctx context.Context, star bool, ids ...string) error {
|
||||
|
||||
return r.ds.WithTx(func(tx model.DataStore) error {
|
||||
for _, id := range ids {
|
||||
exist, err := r.ds.Album().Exists(id)
|
||||
exist, err := r.ds.Album(ctx).Exists(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist {
|
||||
err = tx.Annotation().SetStar(star, userId, model.AlbumItemType, ids...)
|
||||
err = tx.Annotation(ctx).SetStar(star, userId, model.AlbumItemType, ids...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
exist, err = r.ds.Artist().Exists(id)
|
||||
exist, err = r.ds.Artist(ctx).Exists(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist {
|
||||
err = tx.Annotation().SetStar(star, userId, model.ArtistItemType, ids...)
|
||||
err = tx.Annotation(ctx).SetStar(star, userId, model.ArtistItemType, ids...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
err = tx.Annotation().SetStar(star, userId, model.MediaItemType, ids...)
|
||||
err = tx.Annotation(ctx).SetStar(star, userId, model.MediaItemType, ids...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -29,15 +29,15 @@ func (s *scrobbler) Register(ctx context.Context, playerId int, trackId string,
|
||||
var mf *model.MediaFile
|
||||
var err error
|
||||
err = s.ds.WithTx(func(tx model.DataStore) error {
|
||||
mf, err = s.ds.MediaFile().Get(trackId)
|
||||
mf, err = s.ds.MediaFile(ctx).Get(trackId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.ds.Annotation().IncPlayCount(userId, model.MediaItemType, trackId, playTime)
|
||||
err = s.ds.Annotation(ctx).IncPlayCount(userId, model.MediaItemType, trackId, playTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.ds.Annotation().IncPlayCount(userId, model.AlbumItemType, mf.AlbumID, playTime)
|
||||
err = s.ds.Annotation(ctx).IncPlayCount(userId, model.AlbumItemType, mf.AlbumID, playTime)
|
||||
return err
|
||||
})
|
||||
return mf, err
|
||||
@@ -45,7 +45,7 @@ func (s *scrobbler) Register(ctx context.Context, playerId int, trackId string,
|
||||
|
||||
// TODO Validate if NowPlaying still works after all refactorings
|
||||
func (s *scrobbler) NowPlaying(ctx context.Context, playerId int, playerName, trackId, username string) (*model.MediaFile, error) {
|
||||
mf, err := s.ds.MediaFile().Get(trackId)
|
||||
mf, err := s.ds.MediaFile(ctx).Get(trackId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func NewSearch(ds model.DataStore) Search {
|
||||
|
||||
func (s *search) SearchArtist(ctx context.Context, q string, offset int, size int) (Entries, error) {
|
||||
q = sanitize.Accents(strings.ToLower(strings.TrimSuffix(q, "*")))
|
||||
artists, err := s.ds.Artist().Search(q, offset, size)
|
||||
artists, err := s.ds.Artist(ctx).Search(q, offset, size)
|
||||
if len(artists) == 0 || err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func (s *search) SearchArtist(ctx context.Context, q string, offset int, size in
|
||||
for i, al := range artists {
|
||||
artistIds[i] = al.ID
|
||||
}
|
||||
annMap, err := s.ds.Annotation().GetMap(getUserID(ctx), model.ArtistItemType, artistIds)
|
||||
annMap, err := s.ds.Annotation(ctx).GetMap(getUserID(ctx), model.ArtistItemType, artistIds)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -44,7 +44,7 @@ func (s *search) SearchArtist(ctx context.Context, q string, offset int, size in
|
||||
|
||||
func (s *search) SearchAlbum(ctx context.Context, q string, offset int, size int) (Entries, error) {
|
||||
q = sanitize.Accents(strings.ToLower(strings.TrimSuffix(q, "*")))
|
||||
albums, err := s.ds.Album().Search(q, offset, size)
|
||||
albums, err := s.ds.Album(ctx).Search(q, offset, size)
|
||||
if len(albums) == 0 || err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func (s *search) SearchAlbum(ctx context.Context, q string, offset int, size int
|
||||
for i, al := range albums {
|
||||
albumIds[i] = al.ID
|
||||
}
|
||||
annMap, err := s.ds.Annotation().GetMap(getUserID(ctx), model.AlbumItemType, albumIds)
|
||||
annMap, err := s.ds.Annotation(ctx).GetMap(getUserID(ctx), model.AlbumItemType, albumIds)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func (s *search) SearchAlbum(ctx context.Context, q string, offset int, size int
|
||||
|
||||
func (s *search) SearchSong(ctx context.Context, q string, offset int, size int) (Entries, error) {
|
||||
q = sanitize.Accents(strings.ToLower(strings.TrimSuffix(q, "*")))
|
||||
mediaFiles, err := s.ds.MediaFile().Search(q, offset, size)
|
||||
mediaFiles, err := s.ds.MediaFile(ctx).Search(q, offset, size)
|
||||
if len(mediaFiles) == 0 || err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func (s *search) SearchSong(ctx context.Context, q string, offset int, size int)
|
||||
for i, mf := range mediaFiles {
|
||||
trackIds[i] = mf.ID
|
||||
}
|
||||
annMap, err := s.ds.Annotation().GetMap(getUserID(ctx), model.MediaItemType, trackIds)
|
||||
annMap, err := s.ds.Annotation(ctx).GetMap(getUserID(ctx), model.MediaItemType, trackIds)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ type users struct {
|
||||
}
|
||||
|
||||
func (u *users) Authenticate(ctx context.Context, username, pass, token, salt string) (*model.User, error) {
|
||||
user, err := u.ds.User().FindByUsername(username)
|
||||
user, err := u.ds.User(ctx).FindByUsername(username)
|
||||
if err == model.ErrNotFound {
|
||||
return nil, model.ErrInvalidAuth
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func (u *users) Authenticate(ctx context.Context, username, pass, token, salt st
|
||||
return nil, model.ErrInvalidAuth
|
||||
}
|
||||
go func() {
|
||||
err := u.ds.User().UpdateLastAccessAt(user.ID)
|
||||
err := u.ds.User(ctx).UpdateLastAccessAt(user.ID)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Could not update user's lastAccessAt", "user", user.UserName)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/deluan/rest"
|
||||
)
|
||||
|
||||
@@ -22,17 +24,17 @@ type ResourceRepository interface {
|
||||
}
|
||||
|
||||
type DataStore interface {
|
||||
Album() AlbumRepository
|
||||
Artist() ArtistRepository
|
||||
MediaFile() MediaFileRepository
|
||||
MediaFolder() MediaFolderRepository
|
||||
Genre() GenreRepository
|
||||
Playlist() PlaylistRepository
|
||||
Property() PropertyRepository
|
||||
User() UserRepository
|
||||
Annotation() AnnotationRepository
|
||||
Album(ctx context.Context) AlbumRepository
|
||||
Artist(ctx context.Context) ArtistRepository
|
||||
MediaFile(ctx context.Context) MediaFileRepository
|
||||
MediaFolder(ctx context.Context) MediaFolderRepository
|
||||
Genre(ctx context.Context) GenreRepository
|
||||
Playlist(ctx context.Context) PlaylistRepository
|
||||
Property(ctx context.Context) PropertyRepository
|
||||
User(ctx context.Context) UserRepository
|
||||
Annotation(ctx context.Context) AnnotationRepository
|
||||
|
||||
Resource(model interface{}) ResourceRepository
|
||||
Resource(ctx context.Context, model interface{}) ResourceRepository
|
||||
|
||||
WithTx(func(tx DataStore) error) error
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package persistence
|
||||
|
||||
import "github.com/deluan/navidrome/model"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/deluan/navidrome/model"
|
||||
)
|
||||
|
||||
type MockDataStore struct {
|
||||
MockedGenre model.GenreRepository
|
||||
@@ -10,54 +14,54 @@ type MockDataStore struct {
|
||||
MockedUser model.UserRepository
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Album() model.AlbumRepository {
|
||||
func (db *MockDataStore) Album(context.Context) model.AlbumRepository {
|
||||
if db.MockedAlbum == nil {
|
||||
db.MockedAlbum = CreateMockAlbumRepo()
|
||||
}
|
||||
return db.MockedAlbum
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Artist() model.ArtistRepository {
|
||||
func (db *MockDataStore) Artist(context.Context) model.ArtistRepository {
|
||||
if db.MockedArtist == nil {
|
||||
db.MockedArtist = CreateMockArtistRepo()
|
||||
}
|
||||
return db.MockedArtist
|
||||
}
|
||||
|
||||
func (db *MockDataStore) MediaFile() model.MediaFileRepository {
|
||||
func (db *MockDataStore) MediaFile(context.Context) model.MediaFileRepository {
|
||||
if db.MockedMediaFile == nil {
|
||||
db.MockedMediaFile = CreateMockMediaFileRepo()
|
||||
}
|
||||
return db.MockedMediaFile
|
||||
}
|
||||
|
||||
func (db *MockDataStore) MediaFolder() model.MediaFolderRepository {
|
||||
func (db *MockDataStore) MediaFolder(context.Context) model.MediaFolderRepository {
|
||||
return struct{ model.MediaFolderRepository }{}
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Genre() model.GenreRepository {
|
||||
func (db *MockDataStore) Genre(context.Context) model.GenreRepository {
|
||||
if db.MockedGenre != nil {
|
||||
return db.MockedGenre
|
||||
}
|
||||
return struct{ model.GenreRepository }{}
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Playlist() model.PlaylistRepository {
|
||||
func (db *MockDataStore) Playlist(context.Context) model.PlaylistRepository {
|
||||
return struct{ model.PlaylistRepository }{}
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Property() model.PropertyRepository {
|
||||
func (db *MockDataStore) Property(context.Context) model.PropertyRepository {
|
||||
return struct{ model.PropertyRepository }{}
|
||||
}
|
||||
|
||||
func (db *MockDataStore) User() model.UserRepository {
|
||||
func (db *MockDataStore) User(context.Context) model.UserRepository {
|
||||
if db.MockedUser == nil {
|
||||
db.MockedUser = &mockedUserRepo{}
|
||||
}
|
||||
return db.MockedUser
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Annotation() model.AnnotationRepository {
|
||||
func (db *MockDataStore) Annotation(context.Context) model.AnnotationRepository {
|
||||
return struct{ model.AnnotationRepository }{}
|
||||
}
|
||||
|
||||
@@ -65,7 +69,7 @@ func (db *MockDataStore) WithTx(block func(db model.DataStore) error) error {
|
||||
return block(db)
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Resource(m interface{}) model.ResourceRepository {
|
||||
func (db *MockDataStore) Resource(ctx context.Context, m interface{}) model.ResourceRepository {
|
||||
return struct{ model.ResourceRepository }{}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -41,43 +42,43 @@ func New() model.DataStore {
|
||||
return &SQLStore{}
|
||||
}
|
||||
|
||||
func (db *SQLStore) Album() model.AlbumRepository {
|
||||
func (db *SQLStore) Album(context.Context) model.AlbumRepository {
|
||||
return NewAlbumRepository(db.getOrmer())
|
||||
}
|
||||
|
||||
func (db *SQLStore) Artist() model.ArtistRepository {
|
||||
func (db *SQLStore) Artist(context.Context) model.ArtistRepository {
|
||||
return NewArtistRepository(db.getOrmer())
|
||||
}
|
||||
|
||||
func (db *SQLStore) MediaFile() model.MediaFileRepository {
|
||||
func (db *SQLStore) MediaFile(context.Context) model.MediaFileRepository {
|
||||
return NewMediaFileRepository(db.getOrmer())
|
||||
}
|
||||
|
||||
func (db *SQLStore) MediaFolder() model.MediaFolderRepository {
|
||||
func (db *SQLStore) MediaFolder(context.Context) model.MediaFolderRepository {
|
||||
return NewMediaFolderRepository(db.getOrmer())
|
||||
}
|
||||
|
||||
func (db *SQLStore) Genre() model.GenreRepository {
|
||||
func (db *SQLStore) Genre(context.Context) model.GenreRepository {
|
||||
return NewGenreRepository(db.getOrmer())
|
||||
}
|
||||
|
||||
func (db *SQLStore) Playlist() model.PlaylistRepository {
|
||||
func (db *SQLStore) Playlist(context.Context) model.PlaylistRepository {
|
||||
return NewPlaylistRepository(db.getOrmer())
|
||||
}
|
||||
|
||||
func (db *SQLStore) Property() model.PropertyRepository {
|
||||
func (db *SQLStore) Property(context.Context) model.PropertyRepository {
|
||||
return NewPropertyRepository(db.getOrmer())
|
||||
}
|
||||
|
||||
func (db *SQLStore) User() model.UserRepository {
|
||||
func (db *SQLStore) User(context.Context) model.UserRepository {
|
||||
return NewUserRepository(db.getOrmer())
|
||||
}
|
||||
|
||||
func (db *SQLStore) Annotation() model.AnnotationRepository {
|
||||
func (db *SQLStore) Annotation(context.Context) model.AnnotationRepository {
|
||||
return NewAnnotationRepository(db.getOrmer())
|
||||
}
|
||||
|
||||
func (db *SQLStore) Resource(model interface{}) model.ResourceRepository {
|
||||
func (db *SQLStore) Resource(ctx context.Context, model interface{}) model.ResourceRepository {
|
||||
return NewResource(db.getOrmer(), model, getMappedModel(model))
|
||||
}
|
||||
|
||||
|
||||
@@ -63,21 +63,21 @@ var _ = Describe("Initialize test DB", func() {
|
||||
BeforeSuite(func() {
|
||||
conf.Server.DbPath = ":memory:"
|
||||
ds := New()
|
||||
artistRepo := ds.Artist()
|
||||
artistRepo := ds.Artist(nil)
|
||||
for _, a := range testArtists {
|
||||
err := artistRepo.Put(&a)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
albumRepository := ds.Album()
|
||||
albumRepository := ds.Album(nil)
|
||||
for _, a := range testAlbums {
|
||||
err := albumRepository.Put(&a)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
mediaFileRepository := ds.MediaFile()
|
||||
mediaFileRepository := ds.MediaFile(nil)
|
||||
for _, s := range testSongs {
|
||||
err := mediaFileRepository.Put(&s)
|
||||
if err != nil {
|
||||
|
||||
@@ -60,7 +60,7 @@ func (s *Scanner) RescanAll(fullRescan bool) error {
|
||||
func (s *Scanner) Status() []StatusInfo { return nil }
|
||||
|
||||
func (s *Scanner) getLastModifiedSince(folder string) time.Time {
|
||||
ms, err := s.ds.Property().Get(model.PropLastScan + "-" + folder)
|
||||
ms, err := s.ds.Property(nil).Get(model.PropLastScan + "-" + folder)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
@@ -73,11 +73,11 @@ func (s *Scanner) getLastModifiedSince(folder string) time.Time {
|
||||
|
||||
func (s *Scanner) updateLastModifiedSince(folder string, t time.Time) {
|
||||
millis := t.UnixNano() / int64(time.Millisecond)
|
||||
s.ds.Property().Put(model.PropLastScan+"-"+folder, fmt.Sprint(millis))
|
||||
s.ds.Property(nil).Put(model.PropLastScan+"-"+folder, fmt.Sprint(millis))
|
||||
}
|
||||
|
||||
func (s *Scanner) loadFolders() {
|
||||
fs, _ := s.ds.MediaFolder().GetAll()
|
||||
fs, _ := s.ds.MediaFolder(nil).GetAll()
|
||||
for _, f := range fs {
|
||||
log.Info("Configuring Media Folder", "name", f.Name, "path", f.Path)
|
||||
s.folders[f.Path] = NewTagScanner(f.Path, s.ds)
|
||||
|
||||
@@ -58,16 +58,16 @@ func (s *TagScanner) Scan(ctx context.Context, lastModifiedSince time.Time) erro
|
||||
updatedAlbums := map[string]bool{}
|
||||
|
||||
for _, c := range changed {
|
||||
err := s.processChangedDir(c, updatedArtists, updatedAlbums)
|
||||
err := s.processChangedDir(ctx, c, updatedArtists, updatedAlbums)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(updatedAlbums)+len(updatedArtists) > 100 {
|
||||
err = s.refreshAlbums(updatedAlbums)
|
||||
err = s.refreshAlbums(ctx, updatedAlbums)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.refreshArtists(updatedArtists)
|
||||
err = s.refreshArtists(ctx, updatedArtists)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -76,16 +76,16 @@ func (s *TagScanner) Scan(ctx context.Context, lastModifiedSince time.Time) erro
|
||||
}
|
||||
}
|
||||
for _, c := range deleted {
|
||||
err := s.processDeletedDir(c, updatedArtists, updatedAlbums)
|
||||
err := s.processDeletedDir(ctx, c, updatedArtists, updatedAlbums)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(updatedAlbums)+len(updatedArtists) > 100 {
|
||||
err = s.refreshAlbums(updatedAlbums)
|
||||
err = s.refreshAlbums(ctx, updatedAlbums)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.refreshArtists(updatedArtists)
|
||||
err = s.refreshArtists(ctx, updatedArtists)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -94,22 +94,22 @@ func (s *TagScanner) Scan(ctx context.Context, lastModifiedSince time.Time) erro
|
||||
}
|
||||
}
|
||||
|
||||
err = s.refreshAlbums(updatedAlbums)
|
||||
err = s.refreshAlbums(ctx, updatedAlbums)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.refreshArtists(updatedArtists)
|
||||
err = s.refreshArtists(ctx, updatedArtists)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.ds.Album().PurgeEmpty()
|
||||
err = s.ds.Album(ctx).PurgeEmpty()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.ds.Artist().PurgeEmpty()
|
||||
err = s.ds.Artist(ctx).PurgeEmpty()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -117,30 +117,30 @@ func (s *TagScanner) Scan(ctx context.Context, lastModifiedSince time.Time) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TagScanner) refreshAlbums(updatedAlbums map[string]bool) error {
|
||||
func (s *TagScanner) refreshAlbums(ctx context.Context, updatedAlbums map[string]bool) error {
|
||||
var ids []string
|
||||
for id := range updatedAlbums {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return s.ds.Album().Refresh(ids...)
|
||||
return s.ds.Album(ctx).Refresh(ids...)
|
||||
}
|
||||
|
||||
func (s *TagScanner) refreshArtists(updatedArtists map[string]bool) error {
|
||||
func (s *TagScanner) refreshArtists(ctx context.Context, updatedArtists map[string]bool) error {
|
||||
var ids []string
|
||||
for id := range updatedArtists {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return s.ds.Artist().Refresh(ids...)
|
||||
return s.ds.Artist(ctx).Refresh(ids...)
|
||||
}
|
||||
|
||||
func (s *TagScanner) processChangedDir(dir string, updatedArtists map[string]bool, updatedAlbums map[string]bool) error {
|
||||
func (s *TagScanner) processChangedDir(ctx context.Context, dir string, updatedArtists map[string]bool, updatedAlbums map[string]bool) error {
|
||||
dir = path.Join(s.rootFolder, dir)
|
||||
|
||||
start := time.Now()
|
||||
|
||||
// Load folder's current tracks from DB into a map
|
||||
currentTracks := map[string]model.MediaFile{}
|
||||
ct, err := s.ds.MediaFile().FindByPath(dir)
|
||||
ct, err := s.ds.MediaFile(ctx).FindByPath(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -168,7 +168,7 @@ func (s *TagScanner) processChangedDir(dir string, updatedArtists map[string]boo
|
||||
for _, n := range newTracks {
|
||||
c, ok := currentTracks[n.ID]
|
||||
if !ok || (ok && n.UpdatedAt.After(c.UpdatedAt)) {
|
||||
err := s.ds.MediaFile().Put(&n)
|
||||
err := s.ds.MediaFile(ctx).Put(&n)
|
||||
updatedArtists[n.ArtistID] = true
|
||||
updatedAlbums[n.AlbumID] = true
|
||||
numUpdatedTracks++
|
||||
@@ -182,7 +182,7 @@ func (s *TagScanner) processChangedDir(dir string, updatedArtists map[string]boo
|
||||
// Remaining tracks from DB that are not in the folder are deleted
|
||||
for id := range currentTracks {
|
||||
numPurgedTracks++
|
||||
if err := s.ds.MediaFile().Delete(id); err != nil {
|
||||
if err := s.ds.MediaFile(ctx).Delete(id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -191,10 +191,10 @@ func (s *TagScanner) processChangedDir(dir string, updatedArtists map[string]boo
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TagScanner) processDeletedDir(dir string, updatedArtists map[string]bool, updatedAlbums map[string]bool) error {
|
||||
func (s *TagScanner) processDeletedDir(ctx context.Context, dir string, updatedArtists map[string]bool, updatedAlbums map[string]bool) error {
|
||||
dir = path.Join(s.rootFolder, dir)
|
||||
|
||||
ct, err := s.ds.MediaFile().FindByPath(dir)
|
||||
ct, err := s.ds.MediaFile(ctx).FindByPath(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -203,7 +203,7 @@ func (s *TagScanner) processDeletedDir(dir string, updatedArtists map[string]boo
|
||||
updatedAlbums[t.AlbumID] = true
|
||||
}
|
||||
|
||||
return s.ds.MediaFile().DeleteByPath(dir)
|
||||
return s.ds.MediaFile(ctx).DeleteByPath(dir)
|
||||
}
|
||||
|
||||
func (s *TagScanner) loadTracks(dirPath string) (model.MediaFiles, error) {
|
||||
|
||||
@@ -64,7 +64,7 @@ func (app *Router) routes() http.Handler {
|
||||
|
||||
func (app *Router) R(r chi.Router, pathPrefix string, model interface{}) {
|
||||
constructor := func(ctx context.Context) rest.Repository {
|
||||
return app.ds.Resource(model)
|
||||
return app.ds.Resource(ctx, model)
|
||||
}
|
||||
r.Route(pathPrefix, func(r chi.Router) {
|
||||
r.Get("/", rest.GetAll(constructor))
|
||||
|
||||
@@ -41,7 +41,7 @@ func Login(ds model.DataStore) func(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func handleLogin(ds model.DataStore, username string, password string, w http.ResponseWriter, r *http.Request) {
|
||||
user, err := validateLogin(ds.User(), username, password)
|
||||
user, err := validateLogin(ds.User(r.Context()), username, password)
|
||||
if err != nil {
|
||||
rest.RespondWithError(w, http.StatusInternalServerError, "Unknown error authentication user. Please try again")
|
||||
return
|
||||
@@ -89,7 +89,7 @@ func CreateAdmin(ds model.DataStore) func(w http.ResponseWriter, r *http.Request
|
||||
rest.RespondWithError(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
c, err := ds.User().CountAll()
|
||||
c, err := ds.User(r.Context()).CountAll()
|
||||
if err != nil {
|
||||
rest.RespondWithError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -98,7 +98,7 @@ func CreateAdmin(ds model.DataStore) func(w http.ResponseWriter, r *http.Request
|
||||
rest.RespondWithError(w, http.StatusForbidden, "Cannot create another first admin")
|
||||
return
|
||||
}
|
||||
err = createDefaultUser(ds, username, password)
|
||||
err = createDefaultUser(r.Context(), ds, username, password)
|
||||
if err != nil {
|
||||
rest.RespondWithError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -107,7 +107,7 @@ func CreateAdmin(ds model.DataStore) func(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
func createDefaultUser(ds model.DataStore, username, password string) error {
|
||||
func createDefaultUser(ctx context.Context, ds model.DataStore, username, password string) error {
|
||||
id, _ := uuid.NewRandom()
|
||||
log.Warn("Creating initial user", "user", consts.InitialUserName)
|
||||
initialUser := model.User{
|
||||
@@ -118,7 +118,7 @@ func createDefaultUser(ds model.DataStore, username, password string) error {
|
||||
Password: password,
|
||||
IsAdmin: true,
|
||||
}
|
||||
err := ds.User().Put(&initialUser)
|
||||
err := ds.User(ctx).Put(&initialUser)
|
||||
if err != nil {
|
||||
log.Error("Could not create initial user", "user", initialUser, err)
|
||||
}
|
||||
@@ -127,7 +127,7 @@ func createDefaultUser(ds model.DataStore, username, password string) error {
|
||||
|
||||
func initTokenAuth(ds model.DataStore) {
|
||||
once.Do(func() {
|
||||
secret, err := ds.Property().DefaultGet(consts.JWTSecretKey, "not so secret")
|
||||
secret, err := ds.Property(nil).DefaultGet(consts.JWTSecretKey, "not so secret")
|
||||
if err != nil {
|
||||
log.Error("No JWT secret found in DB. Setting a temp one, but please report this error", err)
|
||||
}
|
||||
@@ -190,7 +190,7 @@ func getToken(ds model.DataStore, ctx context.Context) (*jwt.Token, error) {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
c, err := ds.User().CountAll()
|
||||
c, err := ds.User(ctx).CountAll()
|
||||
firstTime := c == 0 && err == nil
|
||||
if firstTime {
|
||||
return nil, ErrFirstTime
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
func initialSetup(ds model.DataStore) {
|
||||
_ = ds.WithTx(func(tx model.DataStore) error {
|
||||
_, err := ds.Property().Get(consts.InitialSetupFlagKey)
|
||||
_, err := ds.Property(nil).Get(consts.InitialSetupFlagKey)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -20,19 +20,19 @@ func initialSetup(ds model.DataStore) {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ds.Property().Put(consts.InitialSetupFlagKey, time.Now().String())
|
||||
err = ds.Property(nil).Put(consts.InitialSetupFlagKey, time.Now().String())
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func createJWTSecret(ds model.DataStore) error {
|
||||
_, err := ds.Property().Get(consts.JWTSecretKey)
|
||||
_, err := ds.Property(nil).Get(consts.JWTSecretKey)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
jwtSecret, _ := uuid.NewRandom()
|
||||
log.Warn("Creating JWT secret, used for encrypting UI sessions")
|
||||
err = ds.Property().Put(consts.JWTSecretKey, jwtSecret.String())
|
||||
err = ds.Property(nil).Put(consts.JWTSecretKey, jwtSecret.String())
|
||||
if err != nil {
|
||||
log.Error("Could not save JWT secret in DB", err)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ var _ = Describe("AlbumListController", func() {
|
||||
|
||||
Describe("GetAlbumList", func() {
|
||||
It("should return list of the type specified", func() {
|
||||
r := newTestRequest("type=newest", "offset=10", "size=20")
|
||||
r := newGetRequest("type=newest", "offset=10", "size=20")
|
||||
listGen.data = engine.Entries{
|
||||
{Id: "1"}, {Id: "2"},
|
||||
}
|
||||
@@ -54,7 +54,7 @@ var _ = Describe("AlbumListController", func() {
|
||||
})
|
||||
|
||||
It("should fail if missing type parameter", func() {
|
||||
r := newTestRequest()
|
||||
r := newGetRequest()
|
||||
_, err := controller.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).To(MatchError("Required string parameter 'type' is not present"))
|
||||
@@ -62,7 +62,7 @@ var _ = Describe("AlbumListController", func() {
|
||||
|
||||
It("should return error if call fails", func() {
|
||||
listGen.err = errors.New("some issue")
|
||||
r := newTestRequest("type=newest")
|
||||
r := newGetRequest("type=newest")
|
||||
|
||||
_, err := controller.GetAlbumList(w, r)
|
||||
|
||||
@@ -72,7 +72,7 @@ var _ = Describe("AlbumListController", func() {
|
||||
|
||||
Describe("GetAlbumList2", func() {
|
||||
It("should return list of the type specified", func() {
|
||||
r := newTestRequest("type=newest", "offset=10", "size=20")
|
||||
r := newGetRequest("type=newest", "offset=10", "size=20")
|
||||
listGen.data = engine.Entries{
|
||||
{Id: "1"}, {Id: "2"},
|
||||
}
|
||||
@@ -86,7 +86,7 @@ var _ = Describe("AlbumListController", func() {
|
||||
})
|
||||
|
||||
It("should fail if missing type parameter", func() {
|
||||
r := newTestRequest()
|
||||
r := newGetRequest()
|
||||
_, err := controller.GetAlbumList2(w, r)
|
||||
|
||||
Expect(err).To(MatchError("Required string parameter 'type' is not present"))
|
||||
@@ -94,7 +94,7 @@ var _ = Describe("AlbumListController", func() {
|
||||
|
||||
It("should return error if call fails", func() {
|
||||
listGen.err = errors.New("some issue")
|
||||
r := newTestRequest("type=newest")
|
||||
r := newGetRequest("type=newest")
|
||||
|
||||
_, err := controller.GetAlbumList2(w, r)
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ func (api *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (api *Router) routes() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(postFormToQueryParams)
|
||||
r.Use(checkRequiredParameters)
|
||||
|
||||
// Add validation middleware if not disabled
|
||||
|
||||
@@ -42,7 +42,7 @@ var _ = Describe("MediaRetrievalController", func() {
|
||||
Describe("GetCoverArt", func() {
|
||||
It("should return data for that id", func() {
|
||||
cover.data = "image data"
|
||||
r := newTestRequest("id=34", "size=128")
|
||||
r := newGetRequest("id=34", "size=128")
|
||||
_, err := controller.GetCoverArt(w, r)
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
@@ -52,7 +52,7 @@ var _ = Describe("MediaRetrievalController", func() {
|
||||
})
|
||||
|
||||
It("should fail if missing id parameter", func() {
|
||||
r := newTestRequest()
|
||||
r := newGetRequest()
|
||||
_, err := controller.GetCoverArt(w, r)
|
||||
|
||||
Expect(err).To(MatchError("id parameter required"))
|
||||
@@ -60,7 +60,7 @@ var _ = Describe("MediaRetrievalController", func() {
|
||||
|
||||
It("should fail when the file is not found", func() {
|
||||
cover.err = model.ErrNotFound
|
||||
r := newTestRequest("id=34", "size=128")
|
||||
r := newGetRequest("id=34", "size=128")
|
||||
_, err := controller.GetCoverArt(w, r)
|
||||
|
||||
Expect(err).To(MatchError("Cover not found"))
|
||||
@@ -68,7 +68,7 @@ var _ = Describe("MediaRetrievalController", func() {
|
||||
|
||||
It("should fail when there is an unknown error", func() {
|
||||
cover.err = errors.New("weird error")
|
||||
r := newTestRequest("id=34", "size=128")
|
||||
r := newGetRequest("id=34", "size=128")
|
||||
_, err := controller.GetCoverArt(w, r)
|
||||
|
||||
Expect(err).To(MatchError("Internal Error"))
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/deluan/navidrome/engine"
|
||||
"github.com/deluan/navidrome/log"
|
||||
@@ -11,6 +13,24 @@ import (
|
||||
"github.com/deluan/navidrome/server/subsonic/responses"
|
||||
)
|
||||
|
||||
func postFormToQueryParams(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
SendError(w, r, NewError(responses.ErrorGeneric, err.Error()))
|
||||
}
|
||||
var parts []string
|
||||
for key, values := range r.Form {
|
||||
for _, v := range values {
|
||||
parts = append(parts, url.QueryEscape(key)+"="+url.QueryEscape(v))
|
||||
}
|
||||
}
|
||||
r.URL.RawQuery = strings.Join(parts, "&")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func checkRequiredParameters(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requiredParameters := []string{"u", "v", "c"}
|
||||
|
||||
@@ -13,8 +13,18 @@ import (
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func newTestRequest(queryParams ...string) *http.Request {
|
||||
r := httptest.NewRequest("get", "/ping?"+strings.Join(queryParams, "&"), nil)
|
||||
func newGetRequest(queryParams ...string) *http.Request {
|
||||
r := httptest.NewRequest("GET", "/ping?"+strings.Join(queryParams, "&"), nil)
|
||||
ctx := r.Context()
|
||||
return r.WithContext(log.NewContext(ctx))
|
||||
}
|
||||
|
||||
func newPostRequest(queryParam string, formFields ...string) *http.Request {
|
||||
r, err := http.NewRequest("POST", "/ping?"+queryParam, strings.NewReader(strings.Join(formFields, "&")))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
|
||||
ctx := r.Context()
|
||||
return r.WithContext(log.NewContext(ctx))
|
||||
}
|
||||
@@ -28,9 +38,37 @@ var _ = Describe("Middlewares", func() {
|
||||
w = httptest.NewRecorder()
|
||||
})
|
||||
|
||||
Describe("ParsePostForm", func() {
|
||||
It("converts any filed in a x-www-form-urlencoded POST into query params", func() {
|
||||
r := newPostRequest("a=abc", "u=user", "v=1.15", "c=test")
|
||||
cp := postFormToQueryParams(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
Expect(next.req.URL.Query().Get("a")).To(Equal("abc"))
|
||||
Expect(next.req.URL.Query().Get("u")).To(Equal("user"))
|
||||
Expect(next.req.URL.Query().Get("v")).To(Equal("1.15"))
|
||||
Expect(next.req.URL.Query().Get("c")).To(Equal("test"))
|
||||
})
|
||||
It("adds repeated params", func() {
|
||||
r := newPostRequest("a=abc", "id=1", "id=2")
|
||||
cp := postFormToQueryParams(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
Expect(next.req.URL.Query().Get("a")).To(Equal("abc"))
|
||||
Expect(next.req.URL.Query()["id"]).To(ConsistOf("1", "2"))
|
||||
})
|
||||
It("overrides query params with same key", func() {
|
||||
r := newPostRequest("a=query", "a=body")
|
||||
cp := postFormToQueryParams(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
Expect(next.req.URL.Query().Get("a")).To(Equal("body"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("CheckParams", func() {
|
||||
It("passes when all required params are available", func() {
|
||||
r := newTestRequest("u=user", "v=1.15", "c=test")
|
||||
r := newGetRequest("u=user", "v=1.15", "c=test")
|
||||
cp := checkRequiredParameters(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
@@ -41,7 +79,7 @@ var _ = Describe("Middlewares", func() {
|
||||
})
|
||||
|
||||
It("fails when user is missing", func() {
|
||||
r := newTestRequest("v=1.15", "c=test")
|
||||
r := newGetRequest("v=1.15", "c=test")
|
||||
cp := checkRequiredParameters(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
@@ -50,7 +88,7 @@ var _ = Describe("Middlewares", func() {
|
||||
})
|
||||
|
||||
It("fails when version is missing", func() {
|
||||
r := newTestRequest("u=user", "c=test")
|
||||
r := newGetRequest("u=user", "c=test")
|
||||
cp := checkRequiredParameters(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
@@ -59,7 +97,7 @@ var _ = Describe("Middlewares", func() {
|
||||
})
|
||||
|
||||
It("fails when client is missing", func() {
|
||||
r := newTestRequest("u=user", "v=1.15")
|
||||
r := newGetRequest("u=user", "v=1.15")
|
||||
cp := checkRequiredParameters(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
@@ -75,7 +113,7 @@ var _ = Describe("Middlewares", func() {
|
||||
})
|
||||
|
||||
It("passes all parameters to users.Authenticate ", func() {
|
||||
r := newTestRequest("u=valid", "p=password", "t=token", "s=salt")
|
||||
r := newGetRequest("u=valid", "p=password", "t=token", "s=salt")
|
||||
cp := authenticate(mockedUser)(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
@@ -89,7 +127,7 @@ var _ = Describe("Middlewares", func() {
|
||||
})
|
||||
|
||||
It("fails authentication with wrong password", func() {
|
||||
r := newTestRequest("u=invalid", "", "", "")
|
||||
r := newGetRequest("u=invalid", "", "", "")
|
||||
cp := authenticate(mockedUser)(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const UserEdit = (props) => (
|
||||
<SimpleForm>
|
||||
<TextInput source="userName" validate={[required()]} />
|
||||
<TextInput source="name" validate={[required()]} />
|
||||
<TextInput source="email" validate={[required(), email()]} />
|
||||
<TextInput source="email" validate={[email()]} />
|
||||
<PasswordInput source="password" validate={[required()]} />
|
||||
<BooleanInput source="isAdmin" initialValue={false} />
|
||||
<DateField source="lastLoginAt" showTime />
|
||||
|
||||
Reference in New Issue
Block a user