Compare commits

..

11 Commits

Author SHA1 Message Date
Deluan
1278863416 feat: support clients that send the API params as a x-www-form-urlencoded POST 2020-01-27 15:10:46 -05:00
Deluan
cffae3a7d6 ci: don't add 'ci:' commits to the changelog 2020-01-27 09:44:28 -05:00
Deluan
0d2911daf9 refactor: add Context to the persistence layer 2020-01-27 09:41:33 -05:00
Deluan
3c54b776d6 docs: add Discord invite 2020-01-27 08:05:15 -05:00
Deluan Quintão
34127805ce doc: update compatibility table 2020-01-27 04:32:55 -05:00
Deluan
ac4aa1ebe2 feat: PORT env var can override configured port 2020-01-26 22:18:30 -05:00
Deluan
b7d7251cf4 fix: user's email is not mandatory 2020-01-26 22:17:58 -05:00
Deluan
7bf110f22f docs: update readme 2020-01-26 21:54:49 -05:00
Deluan
3b651a3728 docs: update readme 2020-01-26 21:34:53 -05:00
Deluan
13390c2edb fix: add git sha and tag to built image 2020-01-26 20:52:09 -05:00
Deluan
221d320ccf docs: add latest released version to README 2020-01-26 20:51:41 -05:00
29 changed files with 244 additions and 168 deletions

View File

@@ -84,3 +84,4 @@ changelog:
exclude:
- '^docs:'
- '^test:'
- '^ci:'

View File

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

View File

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

View File

@@ -1,14 +1,17 @@
# Navidrome Music Streamer
[![Build Status](https://github.com/deluan/navidrome/workflows/Build/badge.svg)](https://github.com/deluan/navidrome/actions)
[![Build](https://img.shields.io/github/workflow/status/deluan/navidrome/Build?style=for-the-badge)](https://github.com/deluan/navidrome/actions)
[![Last Release](https://img.shields.io/github/v/release/deluan/navidrome?label=latest&style=for-the-badge)](https://github.com/deluan/navidrome/releases)
[![Docker Pulls](https://img.shields.io/docker/pulls/deluan/navidrome?style=for-the-badge)](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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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