Compare commits

..

172 Commits

Author SHA1 Message Date
Deluan
7cf98af9da Refactor included handling 2023-06-07 10:49:56 -04:00
Deluan
40f7170930 Add descriptions to included fields 2023-06-07 10:49:56 -04:00
Deluan
4a8f176f0d Add includes for /albums 2023-06-07 10:49:56 -04:00
Deluan
4507895d58 Add includes for /tracks 2023-06-07 10:49:56 -04:00
Deluan
90c2d7d1cf Add Albums endpoints 2023-06-07 10:49:56 -04:00
Deluan
f62231f728 Fix pipeline 2023-06-07 10:49:55 -04:00
Deluan
fd1e06049f Use redocly/cli to bundle/lint OpenAPI spec 2023-06-07 10:49:55 -04:00
Deluan
7dfe29af5e Build OpenAPI.yaml 2023-06-07 10:49:55 -04:00
Deluan
8e03d7d013 Add authorization to new API 2023-06-07 10:49:55 -04:00
Deluan
960415ed95 Update oapi-codegen 2023-06-07 10:49:55 -04:00
Deluan
ea231fe265 Refactor 2023-06-07 10:49:55 -04:00
Deluan
09e52eba87 Change API name 2023-06-07 10:49:55 -04:00
Deluan
2650e4c27c Finish splitting openapi into multiple files 2023-06-07 10:49:55 -04:00
Deluan
3c0f23e3f2 Fix spec paths 2023-06-07 10:49:55 -04:00
Deluan
250b6dbb33 Start breaking OpenAPI spec 2023-06-07 10:49:55 -04:00
Deluan
be945e010a go mod tidy 2023-06-07 10:49:55 -04:00
Deluan
9308127342 Add more tracks attributes 2023-06-07 10:49:55 -04:00
Deluan
141edb881e Add sort param 2023-06-07 10:49:55 -04:00
Deluan
20a1e3160b Add Album and relationships 2023-06-07 10:49:55 -04:00
Deluan
d4c458d193 Add tests 2023-06-07 10:49:55 -04:00
Deluan
ed87e703ff Build collection Links 2023-06-07 10:49:55 -04:00
Deluan
dcb5725642 Add OpenAPI spec for new API, and wire up a new API handler
Based on Aura and JSONAPI
2023-06-07 10:49:55 -04:00
Deluan
f5d97823e8 Fix original date (TDOR) mapping for ffmpeg extractor 2023-06-06 19:13:45 -04:00
Deluan
d6083dab6e Re-apply "Refactor walkDirTree to use fs.FS" but remove context cancelation logic.
This reverts commit 6b3b4d83ff.
2023-06-04 15:06:19 -04:00
Deluan
6b3b4d83ff Revert "Refactor walkDirTree to use fs.FS"
This reverts commit 3853c3318f.
2023-06-04 14:13:33 -04:00
Deluan
3853c3318f Refactor walkDirTree to use fs.FS 2023-06-03 22:25:19 -04:00
tomleb
257ccc5f43 Allow configuring cache folder (#2357)
* Set all clients to dev_download for make get-music

* Use multiple TranscodingCache instances in tests

This fixes flaky tests. The issue is that the TranscodingCache object
was being reused in tests from media_stream_Internal_test.go and
media_stream_test.go. If tests from the former was run first, the cache
would be filled up, so that when running tests from the latter, the `NON
seekable` test would fail.

* Allow configuring cache folder

This commit introduces a new configuration option to configure the cache
folder. This allows the cache to be in a separate folder such as
/var/cache/navidrome on Linux distributions.

* Fix tests

* Removed unused test setup code

---------

Co-authored-by: Deluan <deluan@deluan.com>
Co-authored-by: Deluan <deluan@navidrome.org>
2023-06-02 17:14:11 -04:00
Deluan
cec5fb0d6c Fix lint errors 2023-06-02 16:44:12 -04:00
Deluan
3fc4313e89 Move string slice functions to slice package as generic functions 2023-06-02 16:30:20 -04:00
Deluan
c4c99b7f75 Make GroupAlbumReleases false by default 2023-05-31 15:40:20 -04:00
Deluan
a984bbbc7a Make SmartPlaylists to always be seen as changed for Subsonic clients. 2023-05-25 09:14:00 -04:00
Deluan
ba067667c9 Fix date formatting to use UTC 2023-05-24 14:47:51 -04:00
Deluan
e38a690632 Order albums by full original date (this time is for real). Fixes #1452 2023-05-23 09:51:02 -04:00
Deluan
7d0656f44a Order albums by full original date. Fixes #1452 2023-05-22 23:50:16 -04:00
Deluan
11f33ff8b6 Update dependencies 2023-05-22 17:26:49 -04:00
Deluan
611363fca7 Add missing translation 2023-05-20 17:35:09 -04:00
Deluan
85d43d2366 Add tests to date roll-ups 2023-05-19 21:22:23 -04:00
Deluan
8faaa3cf91 Use table specs in getDate tests 2023-05-19 17:03:14 -04:00
Deluan
20462c52a5 Restore album "year" translation string 2023-05-19 15:29:30 -04:00
certuna
52b77e4194 Support for Original Date, Release Date & splitting/grouping of album editions (#2162)
* Update AlbumGridView.js

* Update AlbumDetails.js

* Update AlbumDetails.js

* Create DoubleRangeField.js

* Update and rename DoubleRangeField.js to RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update AlbumGridView.js

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update index.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update en.json

* Update en.json

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update AlbumGridView.js

* Update AlbumDetails.js

* Update AlbumSongs.js

* Update ContextMenus.js

* Update SongDatagrid.js

* Update AlbumSongs.js

* Update SongDatagrid.js

* Update SongDatagrid.js

* Update SongDatagrid.js

* Update AlbumSongs.js

* Update SongList.js

* Update playlist_track_repository.go

* Update 20230113000000_release_year.go

* Update PlayButton.js

* Update mediafile_repository.go

* Update album.go

* Update playlist_track_repository.go

* Update playlist_track_repository.go

* Update SongDatagrid.js

* Update 20230113000000_release_year.go

* Update SongDatagrid.js

* Update AlbumSongs.js

* Update SongDatagrid.js

* Update SongDatagrid.js

* Update SongDatagrid.js

* Update SongDatagrid.js

* Update AlbumDetails.js

* Update AlbumSongs.js

* Update AlbumSongs.js

* Update RangeFieldDouble.js

* Update SongDatagrid.js

* Update 20230113000000_release_year.go

* Update 20230113000000_release_year.go

* Update 20230113000000_release_year.go

* Update 20230113000000_release_year.go

* Update AlbumSongs.js

* Update AlbumSongs.js

* Update mapping.go

* Update RangeFieldDouble.js

* Update AlbumGridView.js

* Update AlbumSongs.js

* Update en.json

* Update SongDatagrid.js

* Update SongDatagrid.js

* Update metadata.go

* Update mapping.go

* Update AlbumDetails.js

* Update AlbumGridView.js

* Update RangeFieldDouble.js

* Update mapping.go

* Update metadata.go

* Update mapping.go

* Update AlbumDetails.js

* Update 20230113000000_release_year.go

* Update AlbumDetails.js

* Update en.json

* Update configuration.go

* Update mapping.go

* Update configuration.go

* Update mediafile.go

* Update metadata.go

* Update RangeFieldDouble.js

* Update 20230113000000_release_year.go

* Update configuration.go

* Update mapping.go

* Update mediafile.go

* Update mapping.go

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update 20230113000000_release_year.go

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update mapping.go

* Update metadata.go

* Update album.go

* Update mediafile.go

* Update mediafile.go

* Update album.go

* Update fields.go

* Update mediafile_repository.go

* Update playlist_track_repository.go

* Update AlbumSongs.js

* Update SongDatagrid.js

* Update PlayButton.js

* Update SongList.js

* Update ContextMenus.js

* Update SongDatagrid.js

* Update metadata.go

* Update ArtistShow.js

* Update mapping.go

* Update configuration.go

* Update mapping.go

* Update metadata.go

* Update metadata.go

* Update mapping.go

* Update metadata.go

* Update metadata.go

* Update mapping.go

* Update 20230113000000_release_year.go

* Update 20230113000000_release_year.go

* Update mapping.go

* Update metadata.go

* Update metadata.go

* Update album.go

* Update mediafile.go

* Update AlbumDetails.js

* Update AlbumSongs.js

* Update album.go

* Update mediafile.go

* Update metadata.go

* Update mediafile.go

* Update 20230113000000_release_year.go

* Update 20230113000000_release_year.go

* Update album.go

* Update mediafile.go

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update AlbumGridView.js

* Update en.json

* Update AlbumGridView.js

* Update RangeFieldDouble.js

* Update and rename 20230113000000_release_year.go to 20230113000000_release_date.go

* Update album.go

* Update mediafile.go

* Update fields.go

* Update playlist_track_repository.go

* Update mediafile_repository.go

* Update mapping.go

* Update metadata.go

* Update mapping.go

* Update SongDatagrid.js

* Update RangeFieldDouble.js

* Update index.js

* Update ContextMenus.js

* Update PlayButton.js

* Create FormatDate.js

* Update SongList.js

* Update AlbumDetails.js

* Update AlbumSongs.js

* Update AlbumSongs.js

* Update en.json

* Update AlbumDetails.js

* Update album.go

fixed conflict I think?

* Update mediafile.go

fixed conflict

* Format with goimports

* Update SongDatagrid.js

only show Cat # in desktop view

* Update metadata_internal_test.go

* Update metadata_test.go

* Delete test.mp3

* Add files via upload

mp3 test file with Date, Original Date and Release Date

* Update metadata_test.go

* Update metadata_test.go

* Update metadata_test.go

* Update metadata_test.go

* Update taglib_test.go

* Delete test.mp3

* Add files via upload

file with replaygain & dates

* Update AlbumGridView.js

* Update AlbumDetails.js

* Update AlbumSongs.js

* Update ContextMenus.js

* Update FormatDate.js

* Update PlayButton.js

* Update RangeFieldDouble.js

* Update SongDatagrid.js

* Update AlbumSongs.js

* Update SongDatagrid.js

* Update AlbumSongs.js

* Fix formatting

* Update mapping.go

* Update AlbumSongs.js

* Update SongDatagrid.js

* Update SongDatagrid.js

prettier

* Create RangeDoubleField.js

rename of RangeFieldDouble.js

* Update AlbumGridView.js

RangeFieldDouble -> RangeDoubleField

* Update mediafile.go

AllOrNothing() -> allOrNothing()

* Update metadata_internal_test.go

getYear -> getDate

* Update AlbumDetails.js

wrote suggested changes

* Update en.json

Editions -> Releases & fixed the field name

* Update configuration.go

Rename Editions -> Releases

* Update 20230113000000_release_date.go

Editions -> Releases

* Update album.go

Editions -> Releases

* Update mediafile.go

Editions -> Releases

* Update AlbumDetails.js

Editions -> Releases

* Update AlbumSongs.js

Editions -> Releases

* Update RangeDoubleField.js

Editions -> Releases

* Update SongDatagrid.js

Editions -> Releases

* Update index.js

FormatFullDate and RangeDoubleField

* Rename FormatDate.js to FormatFullDate.js

* Delete RangeFieldDouble.js

* Update mediafile.go

AllOrNothing -> allOrNothing

* Update mapping.go

Editions -> Releases

* Update AlbumDetails.js

prettier

* Update SongDatagrid.js

showReleaseRow -> showReleaseDivider

* Update AlbumSongs.js

showReleaseRow -> showReleaseDivider for clarity

* Update and rename 20230113000000_release_date.go to 20230515184510_add_release_date.go

- rename the migration file
- fixed the import to goose/v3
- additional db fields for original date & year

* Update 20230515184510_add_release_date.go

* Update fields.go

* Update album.go

* Update mediafile.go

* Update mapping.go

* Update AlbumDetails.js

* Update en.json

* Update AlbumDetails.js

* Update AlbumDetails.js

now hopefully prettier

* Update mapping.go

---------

Co-authored-by: Deluan <deluan@navidrome.org>
2023-05-19 15:27:47 -04:00
Deluan
010ba0d15c Use table specs in ReplayGain tests.
Also use test.mp3 file from Release Date PR, trying to fix a conflict.
2023-05-19 14:49:15 -04:00
Zane van Iperen
9b7fac5147 Update default transcoding commands (#2325)
Changes the default transcoding commands to only use the first audio
stream, instead of the first arbitrary stream.

Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2023-05-19 10:49:29 -04:00
Deluan
be12c12b28 Remove unused Badge component from ActivityPanel icon 2023-05-17 16:29:19 -04:00
Kendall Garner
a19a643c65 Manually add replaygain tags for m4a (#2346)
* manually add replaygain tags for m4a

* Add replaygain tests for m4a, mp4, ogg

* add new valye for bitrate
2023-05-17 16:00:16 -04:00
Deluan
f9b060af18 Removed onBackdropClick deprecated property 2023-05-17 15:48:22 -04:00
Deluan
a3d78e95f2 Fix Monokai theme. Closes #2353 2023-05-17 15:36:30 -04:00
Deluan
d85b06332c Fix build 2023-05-17 13:49:45 -04:00
Deluan
bfa10cab62 Upgrade to Node v18 2023-05-17 13:41:36 -04:00
Deluan
08fcb430e6 Upgrade React-Admin to 3.19.12 2023-05-17 13:18:58 -04:00
Deluan
5d02df62d0 Fix eslint error 2023-05-17 11:57:43 -04:00
Deluan
c3a2e084b3 Update caniuse-lite 2023-05-17 11:54:22 -04:00
Deluan
4296741ec0 Simplify EventStream handling 2023-05-17 11:53:09 -04:00
Deluan
6bee4ed147 Sanitize filenames inside zip files. Fixes #1763 2023-05-16 18:34:15 -04:00
Deluan
e62c3edc1c Revert: Change fix formatting command 2023-05-16 12:34:09 -04:00
Deluan
0a08d0af3b Change fix formatting command 2023-05-16 12:31:09 -04:00
Deluan
ad513354b9 Disable POEditor import job in forks 2023-05-16 10:33:06 -04:00
dependabot[bot]
a70b81f931 Bump github.com/onsi/ginkgo/v2 from 2.9.4 to 2.9.5 (#2352)
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.9.4 to 2.9.5.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.9.4...v2.9.5)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-15 15:12:31 -04:00
dependabot[bot]
0d920c7832 Bump github.com/prometheus/client_golang from 1.14.0 to 1.15.1 (#2342)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.14.0 to 1.15.1.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.14.0...v1.15.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-13 14:34:11 -04:00
dependabot[bot]
957a73e052 Bump github.com/mileusna/useragent from 1.2.1 to 1.3.2 (#2319)
Bumps [github.com/mileusna/useragent](https://github.com/mileusna/useragent) from 1.2.1 to 1.3.2.
- [Release notes](https://github.com/mileusna/useragent/releases)
- [Commits](https://github.com/mileusna/useragent/compare/v1.2.1...v1.3.2)

---
updated-dependencies:
- dependency-name: github.com/mileusna/useragent
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-13 14:32:01 -04:00
dependabot[bot]
abc418eaa2 Bump github.com/onsi/ginkgo/v2 from 2.9.2 to 2.9.4 (#2343)
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.9.2 to 2.9.4.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.9.2...v2.9.4)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-13 14:28:27 -04:00
dependabot[bot]
1128322011 Bump golang.org/x/tools from 0.8.0 to 0.9.1 (#2350)
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.8.0 to 0.9.1.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.8.0...v0.9.1)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-13 14:28:05 -04:00
dependabot[bot]
2e479defd5 Bump github.com/go-chi/httprate from 0.7.1 to 0.7.4 (#2320)
Bumps [github.com/go-chi/httprate](https://github.com/go-chi/httprate) from 0.7.1 to 0.7.4.
- [Release notes](https://github.com/go-chi/httprate/releases)
- [Commits](https://github.com/go-chi/httprate/compare/v0.7.1...v0.7.4)

---
updated-dependencies:
- dependency-name: github.com/go-chi/httprate
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-13 14:24:37 -04:00
dependabot[bot]
8311a7f215 Bump golang.org/x/sync from 0.1.0 to 0.2.0 (#2344)
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.1.0 to 0.2.0.
- [Commits](https://github.com/golang/sync/compare/v0.1.0...v0.2.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-13 14:23:40 -04:00
dependabot[bot]
6ec8f78076 Bump github.com/pressly/goose/v3 from 3.10.0 to 3.11.2 (#2341)
Bumps [github.com/pressly/goose/v3](https://github.com/pressly/goose) from 3.10.0 to 3.11.2.
- [Release notes](https://github.com/pressly/goose/releases)
- [Changelog](https://github.com/pressly/goose/blob/master/.goreleaser.yml)
- [Commits](https://github.com/pressly/goose/compare/v3.10.0...v3.11.2)

---
updated-dependencies:
- dependency-name: github.com/pressly/goose/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-13 14:23:17 -04:00
Logan Marchione
3e879d2a8c Add K8s manifest (#2330)
* Add K8s manifest

* Update README.md
2023-04-29 16:14:44 -04:00
Jeff Henson
6d3d005fca Allow the setrlimit syscall - #1961 (#2333)
This appears to be used by newer go versions and navidrome fails to
start unless it's allowed.

Signed-off-by: Jeff Henson <jeff@henson.io>
2023-04-27 21:30:43 -04:00
Deluan
c12510d6e2 Update README 2023-04-11 14:00:44 -04:00
Deluan
0bd73bd3f4 Better GH Action names 2023-04-11 09:16:25 -04:00
Deluan
8c120ee3c9 Better GH Action names 2023-04-11 09:15:08 -04:00
Deluan
9590b3c25d Use the highest resolution artist image from Spotify 2023-04-10 15:34:22 -04:00
Deluan
4887c33053 Bump golang.org/x packages 2023-04-10 14:07:12 -04:00
Subhajit Ghosh
da21acba92 Give page the right lang attribute (#2299)
* Fixed issue no #2174

Signed-off-by: Subhajit Ghosh <subhajitstd07@gmail.com>

* Fixed issue no #2174

---------

Signed-off-by: Subhajit Ghosh <subhajitstd07@gmail.com>
2023-04-08 13:39:59 -04:00
Deluan Quintão
9154e44eb4 Add initial support for OpenSubsonic. (#2302) 2023-04-08 13:25:37 -04:00
Deluan Quintão
2e01063429 Update translations (#2198)
Co-authored-by: deluan <deluan@users.noreply.github.com>
2023-04-06 22:09:49 -04:00
Deluan
597e5abed6 Fix push develop to Docker Hub 2023-04-06 20:11:35 -04:00
Deluan Quintão
92994efe48 Publish docker images to ghcr.io (#2298)
* Publish all images (including PRs) to GHCR, only releases and `develop` to Docker Hub
2023-04-06 19:53:31 -04:00
Deluan
9628b1389d Add help msg for JS formatting errors 2023-04-06 11:45:32 -04:00
Deluan
347424009d Show Player name, not client, in mobile view. Fix #1659. 2023-04-05 22:48:33 -04:00
Deluan
ecac74c2bd Fix getSongsByGenre pagination. Fix #1640 2023-04-05 22:39:32 -04:00
Deluan
ddfde7bfc8 Run lint on latest Go 1.20.x 2023-04-04 19:13:24 -04:00
Deluan
96c50d369a Upgrade to Go 1.20.3 and GoRelease 1.16.1 2023-04-04 19:10:03 -04:00
Deluan
310c816cdd Use Go 1.20 for local cross-compilation 2023-04-04 15:33:42 -04:00
Deluan
bd402fb2a8 Fix IntelliJ warning 2023-04-04 13:01:32 -04:00
dependabot[bot]
8bb141b730 Bump github.com/spf13/cobra from 1.6.1 to 1.7.0 (#2293)
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.6.1 to 1.7.0.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.6.1...v1.7.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-04 11:04:18 -04:00
Deluan
f25b91b4d8 Remove any previous UNIX socket file 2023-04-04 11:03:37 -04:00
dependabot[bot]
f959701d9d Bump github.com/onsi/gomega from 1.27.5 to 1.27.6 (#2292)
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.27.5 to 1.27.6.
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.27.5...v1.27.6)

---
updated-dependencies:
- dependency-name: github.com/onsi/gomega
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-04 10:55:36 -04:00
Deluan
61dd8d55ca Fix data race in scanner 2023-04-04 10:51:43 -04:00
Deluan
bbb9461000 Increase max Server-Sent Events' ID 2023-04-04 10:46:57 -04:00
Deluan
95016f687e Fix SQL migrations 2023-04-04 10:45:55 -04:00
Deluan
c3cc7dee01 Enable SQL migrations 2023-04-04 10:30:28 -04:00
Deluan
7847f19c9d Upgrade goose 2023-04-04 10:05:31 -04:00
dependabot[bot]
7a0df4429e Bump github.com/onsi/gomega from 1.27.5 to 1.27.6 (#2288)
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.27.5 to 1.27.6.
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.27.5...v1.27.6)

---
updated-dependencies:
- dependency-name: github.com/onsi/gomega
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-03 21:01:39 -04:00
Deluan
6a8d2dc87d Only use valid images for artist.* artwork 2023-04-03 18:07:15 -04:00
Deluan
de816e8e5d Fix lint error 2023-04-03 11:15:46 -04:00
Deluan
b22d0366d5 Use channels for EventStream instead of diodes 2023-04-03 10:51:24 -04:00
Deluan
fea2de8f90 Add Galician translation. 2023-04-02 18:58:44 -04:00
Deluan
d6dd0aaae7 Close SSE connection on write error 2023-04-02 18:40:58 -04:00
Fadeeeeeeee
458017b112 Update Chinese translations (#2260)
* Update Chinese translations

* Update Chinese translations

* Update Chinese translations
2023-04-02 18:40:48 -04:00
Deluan
e6bfa2bb0b Convert our usage of go-diodes into a simplified, generic version 2023-04-01 21:53:45 -04:00
Deluan
1c7fb74a1d Fix writeEvents race condition.
This required removing the compress middleware from the /events route.
2023-04-01 20:54:15 -04:00
Deluan
83ae2ba3e6 Fix race condition 2023-04-01 18:40:37 -04:00
Joakim Repomaa
2ccc5bc941 Implement artist art priority (#2266)
* implement artist art priority

* add tests
2023-03-30 18:28:05 -04:00
Deluan
406554f1c4 Remove some tools from dependencies, reducing the modules dependencies 2023-03-30 15:33:47 -04:00
Deluan
e89cdf6199 Fix flaky tests 2023-03-30 09:25:18 -04:00
Deluan
cf804a52ef Add support for listening on Unix socket.
For that to work, specify the config option `Address` with `unix:/path/to/socket/file`.

Closes #1477
2023-03-29 16:05:59 -04:00
Deluan
628fd69d3d Fix race condition 2023-03-29 15:17:34 -04:00
Deluan
1d00d1e986 Fix writeEvent function.
It would not send anything if the `ResponseWriter` was not a `http.Flusher`, and it was leaking channels with `time.After`
2023-03-29 15:04:40 -04:00
Deluan
607c4067b8 Show translation changes on pipeline 2023-03-29 13:03:37 -04:00
Deluan
e3079d81ea More tests 2023-03-27 20:36:23 -04:00
Deluan
3bedd89c17 Bump dependencies 2023-03-27 14:48:20 -04:00
dependabot[bot]
57829bfa4c Bump github.com/lestrrat-go/jwx/v2 from 2.0.8 to 2.0.9 (#2282)
Bumps [github.com/lestrrat-go/jwx/v2](https://github.com/lestrrat-go/jwx) from 2.0.8 to 2.0.9.
- [Release notes](https://github.com/lestrrat-go/jwx/releases)
- [Changelog](https://github.com/lestrrat-go/jwx/blob/develop/v2/Changes)
- [Commits](https://github.com/lestrrat-go/jwx/compare/v2.0.8...v2.0.9)

---
updated-dependencies:
- dependency-name: github.com/lestrrat-go/jwx/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-27 14:47:32 -04:00
Deluan
b998c05ca0 Some refactorings 2023-03-26 21:28:37 -04:00
Deluan
05d381c26f Add more middleware tests 2023-03-26 21:28:36 -04:00
zayedalsaidi
59a9c056b4 Add Arabic translation (#2277) 2023-03-26 19:56:59 -04:00
Deluan
0de81b8352 Bump caniuse-lite 2023-03-26 19:38:09 -04:00
Deluan
91785ecf36 Add tests for core.Archiver 2023-03-26 19:34:12 -04:00
Deluan
65eeb5ec1a Add tests for serverAddressMiddleware 2023-03-26 13:29:57 -04:00
Julien Voisin
17e0cd5504 Shuffle the tests, just in case (#2272) 2023-03-22 20:12:12 -04:00
Deluan
3a6d2dcd49 More log redaction 2023-03-21 11:16:00 -04:00
Deluan
183b462fed Fix zip comments in Share downloads. 2023-03-21 10:34:04 -04:00
Deluan
16fc4eb792 Fix missing extensions in Share downloads.
See https://github.com/navidrome/navidrome/pull/2246#issuecomment-1476996397
2023-03-21 10:31:00 -04:00
dependabot[bot]
6fee744d99 Bump github.com/onsi/gomega from 1.27.3 to 1.27.4 (#2268)
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.27.3 to 1.27.4.
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.27.3...v1.27.4)

---
updated-dependencies:
- dependency-name: github.com/onsi/gomega
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-20 14:15:32 -04:00
dependabot[bot]
74d5c7bc82 Bump github.com/golangci/golangci-lint from 1.51.2 to 1.52.0 (#2270)
Bumps [github.com/golangci/golangci-lint](https://github.com/golangci/golangci-lint) from 1.51.2 to 1.52.0.
- [Release notes](https://github.com/golangci/golangci-lint/releases)
- [Changelog](https://github.com/golangci/golangci-lint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/golangci/golangci-lint/compare/v1.51.2...v1.52.0)

---
updated-dependencies:
- dependency-name: github.com/golangci/golangci-lint
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-20 14:15:18 -04:00
dependabot[bot]
880fc9e195 Bump github.com/Masterminds/squirrel from 1.5.3 to 1.5.4 (#2269)
Bumps [github.com/Masterminds/squirrel](https://github.com/Masterminds/squirrel) from 1.5.3 to 1.5.4.
- [Release notes](https://github.com/Masterminds/squirrel/releases)
- [Commits](https://github.com/Masterminds/squirrel/compare/v1.5.3...v1.5.4)

---
updated-dependencies:
- dependency-name: github.com/Masterminds/squirrel
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-20 14:15:01 -04:00
Xidorn Quan
1430aa108d Update play_date on scrobble only when newer - #2262 (#2263)
* fix(persistence): Update play_date on scrobble only when newer - #2262

Signed-off-by: Xidorn Quan <me@upsuper.org>

* expand iff

---------

Signed-off-by: Xidorn Quan <me@upsuper.org>
2023-03-18 18:28:01 -04:00
Deluan
673880d661 Add option to load TLS cert/key, and use HTTPS 2023-03-17 16:32:13 -04:00
Deluan
7ea111322b Don't pump the volume up to 100% if it is not in a mobile device. Fix #2255
This detection method is not bullet-proof, but should work for now.

Ref: https://stackoverflow.com/a/3540295
2023-03-16 17:25:07 -04:00
Deluan
377e7ebd52 Disable share downloading when EnableDownloads is false.
Fixes https://github.com/navidrome/navidrome/pull/2246#issuecomment-1472341635
2023-03-16 13:11:26 -04:00
Deluan
23c483da10 Only freezes issues/prs after 120 days 2023-03-15 17:53:54 -04:00
Deluan
c380139606 Fix lint 2023-03-15 13:10:14 -04:00
Deluan
63fbccf5a9 Enable memory profiling 2023-03-15 12:43:25 -04:00
Deluan
1f6ec1d9f5 Add pprof endpoint, disabled by default 2023-03-15 10:56:16 -04:00
dependabot[bot]
cad8156353 Bump webpack from 5.74.0 to 5.76.1 in /ui (#2256)
Bumps [webpack](https://github.com/webpack/webpack) from 5.74.0 to 5.76.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.74.0...v5.76.1)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-15 09:13:22 -04:00
Deluan Quintão
f7d4fcdcc1 Convert all Subsonic API ints to int32 as per specification (#2252)
* Fix Genre

* Fix ArtistID3

* Fix AlbumID3

* Fix Child

* Fix NowPlayingEntry

* Fix Playlist

* Fix Share

* Fix User

* Fix Artist

* Fix Directory

* Fix Error
2023-03-14 09:48:52 -04:00
Deluan Quintão
002cb4ed71 Update README.md 2023-03-13 19:34:47 -04:00
Deluan Quintão
e13eaebbde Update README.md 2023-03-13 19:32:13 -04:00
dependabot[bot]
539c0faedb Bump github.com/onsi/ginkgo/v2 from 2.9.0 to 2.9.1 (#2251)
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.9.0 to 2.9.1.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.9.0...v2.9.1)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-13 14:42:40 -04:00
Moink
4ccb6ccb09 Update Chinese translations (#2250) 2023-03-12 20:24:31 -04:00
Deluan
ec0eb2866b Hide Love button on Artist Page when EnableFavourites=false. Fix #2245 2023-03-10 23:34:02 -05:00
Deluan
b520d8827a Add download button in the SharePlayer 2023-03-10 23:33:29 -05:00
Deluan
a7d3e6e1f1 Add option to allow share to be downloaded 2023-03-10 23:33:29 -05:00
Deluan
a22eef39f7 Add share download endpoint 2023-03-10 23:33:29 -05:00
Torsten Curdt
50d9838652 Add docker compose examples, with traefik or caddy and without, fixes #476 (#2240)
* add docker compose examples, with traefik or caddy and without, fixes #476

* ignore the docker-compose in root, but not the one in contrib
2023-03-10 18:57:09 -05:00
Deluan
016454c217 Bump golangci-lint version 2023-03-10 17:46:05 -05:00
Deluan
41a5db72e7 Update more dependencies 2023-03-10 17:31:13 -05:00
Deluan
6e6ec58429 Update sanitize and golang.org/x dependencies 2023-03-10 17:21:08 -05:00
Deluan
c88e1baa7c Make playlist tracks match case-insensitive. Fix #1720 2023-03-10 12:29:38 -05:00
Deluan
e16e3d2e7b Fix pipeline. 2023-03-09 22:25:56 -05:00
Deluan
339a6239fd Ignore Recycle Bins in Windows. Fix #1074 2023-03-09 22:14:58 -05:00
Deluan
47f15ccbc3 Make AlbumArtists clickable in AlbumSongs view. Fixes #1627 2023-03-09 18:04:07 -05:00
Deluan
9667f3cd48 Add file path to toggleable columns in SongList view. Fix #1719 2023-03-09 17:47:20 -05:00
Deluan
5773fa0349 Fix discussions links 2023-03-08 14:14:42 -05:00
Deluan
527c378c41 Add feature request link to About dialog 2023-03-08 12:41:51 -05:00
Deluan
caa0788853 Fine tune issue templates 2023-03-08 12:27:28 -05:00
Deluan
40b14e6d81 Add log-output to lock-threads bot 2023-03-06 20:12:46 -05:00
Deluan
becd50eb68 Remove debug-only option from stale bot 2023-03-06 20:08:02 -05:00
Deluan
15b5aa9143 Add stale/lock-threads bot 2023-03-06 20:01:42 -05:00
Deluan
7987d982cf Fix pipeline's lint error message 2023-03-06 19:38:20 -05:00
Deluan
1dd074bbb4 Add new issue templates 2023-03-06 17:15:36 -05:00
Deluan
7eac9d2bbe Bump dependencies 2023-03-05 21:09:45 -05:00
dependabot[bot]
362d8c50fe Bump github.com/onsi/gomega from 1.26.0 to 1.27.1 (#2204)
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.26.0 to 1.27.1.
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.26.0...v1.27.1)

---
updated-dependencies:
- dependency-name: github.com/onsi/gomega
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-05 20:25:16 -05:00
dependabot[bot]
01c604ba7b Bump github.com/stretchr/testify from 1.8.1 to 1.8.2 (#2216)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.1 to 1.8.2.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.1...v1.8.2)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-05 20:23:36 -05:00
dependabot[bot]
2c129a2890 Bump golang.org/x/image from 0.0.0-20191009234506-e7c1f5e7dbb8 to 0.5.0 (#2217)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.0.0-20191009234506-e7c1f5e7dbb8 to 0.5.0.
- [Release notes](https://github.com/golang/image/releases)
- [Commits](https://github.com/golang/image/commits/v0.5.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-05 20:23:04 -05:00
Deluan
5fc4076aec Fix translation key 2023-02-16 21:05:11 -05:00
Deluan
d303ad2676 Bump dependencies 2023-02-15 22:46:56 -05:00
Deluan
c4a68c8a0a Fix build pipeline 2023-02-15 22:27:16 -05:00
Deluan
ad9ce98cc2 Use GoLang 1.20.1 in pipeline 2023-02-15 22:21:50 -05:00
Deluan
a134b1b608 Use sync/atomic package, now that we are at Go 1.19 2023-02-15 21:21:59 -05:00
Deluan
6dce4b2478 Remove custom atomic.Bool, we are now at Go 1.19 2023-02-15 21:18:24 -05:00
Deluan
10108c63c9 Allow BaseURL to contain full server url, including scheme and host. Fix #2183 2023-02-15 21:13:38 -05:00
Deluan
aac6e2cb07 Add path to cookies. Fix #1580 2023-02-15 20:23:32 -05:00
Deluan
0ffdb2eee0 Bump minimum Go version to 1.19 2023-02-15 20:20:08 -05:00
404 changed files with 8665 additions and 2332 deletions

View File

@@ -4,10 +4,10 @@
"dockerfile": "Dockerfile",
"args": {
// Update the VARIANT arg to pick a version of Go: 1, 1.15, 1.14
"VARIANT": "1.19",
"VARIANT": "1.20",
// Options
"INSTALL_NODE": "true",
"NODE_VERSION": "v16"
"NODE_VERSION": "v18"
}
},
"workspaceMount": "",

View File

@@ -1,37 +0,0 @@
---
name: Bug Report
about: Use this template for submitting a bug report.
title: ""
labels: bug
assignees: ""
---
<!-- Please check that another issue for the same bug has not been already made by searching the [issues](https://github.com/navidrome/navidrome/issues) -->
### Description
A clear and concise description of what the bug is.
### Expected Behaviour
What you would have expected to happen instead.
### Steps to reproduce
1. Open the '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
### Platform information
- Navidrome version: <!-- e.g. v0.40.0 -->
- Browser and version: <!-- e.g. Firefox v87.0b9 -->
- Operating System: <!-- e.g. Ubuntu 20.04 and whether using a binary, docker or built from source -->
### Additional information
Any other information that may be relevant or give context to the problem.
- Screenshots (if applicable)?
- Logs? <!-- Turn the log level up to trace -->
- Client used? <!-- e.g. DSub v5.5.2R2 -->

103
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,103 @@
name: Bug Report
description: Before opening a new issue, please search to see if an issue already exists for the bug you encountered.
title: "[Bug]: "
labels: ["bug", "triage"]
#assignees:
# - deluan
body:
- type: markdown
attributes:
value: |
### Thanks for taking the time to fill out this bug report!
- type: checkboxes
id: requirements
attributes:
label: "I confirm that:"
options:
- label: I have searched the existing [open AND closed issues](https://github.com/navidrome/navidrome/issues?q=is%3Aissue) to see if an issue already exists for the bug I've encountered
required: true
- label: I'm using the latest version (your issue may have been fixed already)
required: false
- type: input
id: version
attributes:
label: Version
description: What version of Navidrome are you running? (please try upgrading first, as your issue may have been fixed already).
validations:
required: true
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this scenario...
2. With this config...
3. Click (or Execute) '...'
4. See error...
validations:
required: false
- type: textarea
id: env
attributes:
label: Environment
description: |
examples:
- **OS**: Ubuntu 20.04
- **Browser**: Chrome 110.0.5481.177 on Windows 11
- **Client**: DSub 5.5.1
value: |
- OS:
- Browser:
- Client:
render: markdown
- type: dropdown
id: distribution
attributes:
label: How Navidrome is installed?
multiple: false
options:
- Docker
- Binary (from downloads page)
- Package
- Built from sources
validations:
required: true
- type: textarea
id: config
attributes:
label: Configuration
description: Please copy and paste your `navidrome.toml` (and/or `docker-compose.yml`) configuration. This will be automatically formatted into code, so no need for backticks.
render: toml
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output (change your `LogLevel` (`ND_LOGLEVEL`) to debug). This will be automatically formatted into code, so no need for backticks. ([Where I can find the logs?](https://www.navidrome.org/docs/faq/#where-are-the-logs))
render: shell
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach screenshots by clicking this area to highlight it and then dragging files in.
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/navidrome/navidrome/blob/master/CODE_OF_CONDUCT.md).
options:
- label: I agree to follow Navidrome's Code of Conduct
required: true

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Ideas for new features
url: https://github.com/navidrome/navidrome/discussions/categories/ideas
about: This is the place to share and discuss new ideas and potentially new features.
- name: Support requests
url: https://github.com/navidrome/navidrome/discussions/categories/q-a
about: This is the place to ask questions.

View File

@@ -1,24 +0,0 @@
---
name: Feature Request
about: Use this template to request for a feature.
title: ""
labels: enhancement
assignees: ""
---
<!-- Please check that another issue for the same feature request has not been already made by searching the [issues](https://github.com/navidrome/navidrome/issues) -->
### Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. For e.g. I'm always frustrated when '...'
### Describe the solution you'd like
A clear and concise description of what you would like to happen.
### Describe alternative solutions that would also satisfy this problem
A clear and concise description of any alternative solutions or features you've considered.
### Additional context
Add any other context or screenshots about the feature request here.

View File

@@ -1,22 +0,0 @@
#!/bin/bash
GIT_TAG="${GITHUB_REF##refs/tags/}"
GIT_BRANCH="${GITHUB_REF##refs/heads/}"
GIT_SHA=$(git rev-parse --short HEAD)
PR_NUM=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
DOCKER_IMAGE_TAG="--tag ${DOCKER_IMAGE}:sha-${GIT_SHA}"
if [[ $PR_NUM != "null" ]]; then
DOCKER_IMAGE_TAG="${DOCKER_IMAGE_TAG} --tag ${DOCKER_IMAGE}:pr-${PR_NUM}"
fi
if [[ $GITHUB_REF != "$GIT_TAG" ]]; then
DOCKER_IMAGE_TAG="${DOCKER_IMAGE_TAG} --tag ${DOCKER_IMAGE}:${GIT_TAG#v} --tag ${DOCKER_IMAGE}:latest"
elif [[ $GITHUB_REF == "refs/heads/master" ]]; then
DOCKER_IMAGE_TAG="${DOCKER_IMAGE_TAG} --tag ${DOCKER_IMAGE}:develop"
elif [[ $GIT_BRANCH = feature/* ]]; then
DOCKER_IMAGE_TAG="${DOCKER_IMAGE_TAG} --tag ${DOCKER_IMAGE}:$(echo $GIT_BRANCH | tr / -)"
fi
echo ${DOCKER_IMAGE_TAG}

View File

@@ -1,7 +1,7 @@
name: Add download link to PR
on:
workflow_run:
workflows: ['Pipeline']
workflows: ['Pipeline: Test, Lint, Build']
types: [completed]
jobs:
pr_comment:

View File

@@ -1,4 +1,4 @@
name: Pipeline
name: 'Pipeline: Test, Lint, Build'
on:
push:
branches:
@@ -8,6 +8,7 @@ on:
pull_request:
branches:
- master
jobs:
go-lint:
name: Lint Go code
@@ -16,10 +17,10 @@ jobs:
- name: Install taglib
run: sudo apt-get install libtag1-dev
- name: Set up Go 1.19
- name: Set up Go 1.20
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version: 1.20.x
- uses: actions/checkout@v3
@@ -39,10 +40,32 @@ jobs:
run: |
git status --porcelain
if [ -n "$(git status --porcelain)" ]; then
echo "To fix this check, run: goimports -w $(find . -name '*.go' | grep -v '_gen.go$') && go mod tidy"
echo 'To fix this check, run "goimports -w $(find . -name '*.go' | grep -v '_gen.go$') && go mod tidy"'
exit 1
fi
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Build and Lint OpenAPI spec
run: make lintapi gen
- name: Verify no changes
run: |
git status --porcelain
if [ -n "$(git status --porcelain)" ]; then
echo 'Changes to OpenAPI spec caused changes to the code. Please review and commit the changes.'
exit 1
fi
- uses: actions/upload-artifact@v3
with:
name: openapi.yaml
path: api/openapi.yaml
go:
name: Test with Go ${{ matrix.go_version }}
runs-on: ubuntu-latest
@@ -69,7 +92,7 @@ jobs:
- name: Test
continue-on-error: ${{contains(matrix.go_version, 'beta') || contains(matrix.go_version, 'rc')}}
run: go test -race -cover ./... -v
run: go test -shuffle=on -race -cover ./... -v
js:
name: Build JS bundle
@@ -80,7 +103,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -108,7 +131,6 @@ jobs:
with:
name: js-bundle
path: ui/build
retention-days: 7
binaries:
name: Build binaries
@@ -126,7 +148,7 @@ jobs:
path: ui/build
- name: Config /github/workspace folder as trusted
uses: docker://deluan/ci-goreleaser:1.19.5-1
uses: docker://deluan/ci-goreleaser:1.20.3-1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -134,7 +156,7 @@ jobs:
- name: Run GoReleaser - SNAPSHOT
if: startsWith(github.ref, 'refs/tags/') != true
uses: docker://deluan/ci-goreleaser:1.19.5-1
uses: docker://deluan/ci-goreleaser:1.20.3-1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -142,7 +164,7 @@ jobs:
- name: Run GoReleaser - RELEASE
if: startsWith(github.ref, 'refs/tags/')
uses: docker://deluan/ci-goreleaser:1.19.5-1
uses: docker://deluan/ci-goreleaser:1.20.3-1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -155,10 +177,9 @@ jobs:
dist
!dist/*.tar.gz
!dist/*.zip
retention-days: 7
docker:
name: Build Docker images
name: Build and publish Docker images
needs: [binaries]
runs-on: ubuntu-latest
env:
@@ -183,11 +204,42 @@ jobs:
name: binaries
path: dist
- name: Build the Docker image and push
- name: Login to Docker Hub
if: env.DOCKER_IMAGE != ''
env:
DOCKER_IMAGE: ${{secrets.DOCKER_IMAGE}}
DOCKER_PLATFORM: linux/amd64,linux/386,linux/arm/v6,linux/arm/v7,linux/arm64
run: |
echo ${{secrets.DOCKER_PASSWORD}} | docker login -u ${{secrets.DOCKER_USERNAME}} --password-stdin
docker buildx build --platform ${DOCKER_PLATFORM} `.github/workflows/docker-tags.sh` -f .github/workflows/pipeline.dockerfile --push .
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: env.DOCKER_IMAGE != ''
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
if: env.DOCKER_IMAGE != ''
id: meta
uses: docker/metadata-action@v4
with:
labels: |
maintainer=deluan
images: |
name=${{secrets.DOCKER_IMAGE}},enable=${{env.GITHUB_REF_TYPE == 'tag' || github.ref == format('refs/heads/{0}', 'master')}}
name=ghcr.io/${{ github.repository }}
tags: |
type=ref,event=pr
type=semver,pattern={{version}}
type=raw,value=develop,enable={{is_default_branch}}
- name: Build and Push
if: env.DOCKER_IMAGE != ''
uses: docker/build-push-action@v4
with:
context: .
file: .github/workflows/pipeline.dockerfile
platforms: linux/amd64,linux/386,linux/arm/v6,linux/arm/v7,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}

55
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: 'Close stale issues and PRs'
on:
workflow_dispatch:
schedule:
- cron: '30 1 * * *'
permissions:
contents: read
jobs:
stale:
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v4.0.0
with:
issue-inactive-days: 120
pr-inactive-days: 120
log-output: true
add-issue-labels: 'frozen-due-to-age'
add-pr-labels: 'frozen-due-to-age'
issue-comment: >
This issue has been automatically locked since there
has not been any recent activity after it was closed.
Please open a new issue for related bugs.
pr-comment: >
This pull request has been automatically locked since there
has not been any recent activity after it was closed.
Please open a new issue for related bugs.
- uses: actions/stale@v7
with:
operations-per-run: 999
days-before-issue-stale: 180
days-before-pr-stale: 180
days-before-issue-close: 30
days-before-pr-close: 30
stale-issue-message: >
This issue has been automatically marked as stale because it has not had
recent activity. The resources of the Navidrome team are limited, and so we are asking for your help.
If this is a **bug** and you can still reproduce this error on the <code>master</code> branch, please reply with all of the information you have about it in order to keep the issue open.
If this is a **feature request**, and you feel that it is still relevant and valuable, please tell us why.
This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
stale-pr-message: This PR has been automatically marked as stale because it has not had
recent activity. The resources of the Navidrome team are limited, and so we are asking for your help.
Please check https://github.com/navidrome/navidrome/blob/master/CONTRIBUTING.md#pull-requests and verify that this code contribution fits with the description. If yes, tell it in a comment.
This PR will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
stale-issue-label: 'stale'
exempt-issue-labels: 'keep,security'
stale-pr-label: 'stale'
exempt-pr-labels: 'keep,security'

View File

@@ -1,4 +1,4 @@
name: Update Translations
name: POEditor import
on:
workflow_dispatch:
schedule:
@@ -6,6 +6,7 @@ on:
jobs:
update-translations:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'navidrome' }}
steps:
- uses: actions/checkout@v3
- name: Get updated translations
@@ -14,6 +15,10 @@ jobs:
POEDITOR_APIKEY: ${{ secrets.POEDITOR_APIKEY }}
run: |
./update-translations.sh
- name: Show changes, if any
run: |
git status --porcelain
git diff
- name: Create Pull Request
uses: peter-evans/create-pull-request@v4
with:

5
.gitignore vendored
View File

@@ -14,12 +14,15 @@ navidrome.toml
master.zip
testDB
navidrome.db
cache/*
*.swp
embedded_gen.go
dist
music
docker-compose.yml
navidrome.db-shm
navidrome.db-wal
tags
.gitinfo
docker-compose.yml
!contrib/docker-compose.yml
/api/openapi.yaml

View File

@@ -7,7 +7,6 @@ linters:
- asciicheck
- bidichk
- bodyclose
- depguard
- dogsled
- durationcheck
- errcheck

2
.nvmrc
View File

@@ -1 +1 @@
v16
v18

View File

@@ -2,12 +2,15 @@
Navidrome is a streaming service which allows you to enjoy your music collection from anywhere. We'd welcome you to contribute to our open source project and make Navidrome even better. There are some basic guidelines which you need to follow if you like to contribute to Navidrome.
- [Asking Support Questions](#asking-support-questions)
- [Code of Conduct](#code-of-conduct)
- [Issues](#issues)
- [Questions](#questions)
- [Pull Requests](#pull-requests)
## Asking Support Questions
We have an active [discussion forum](https://github.com/navidrome/navidrome/discussions) where users and developers can ask questions. Please don't use the GitHub issue tracker to ask questions.
## Code of Conduct
Please read the following [Code of Conduct](https://github.com/navidrome/navidrome/blob/master/CODE_OF_CONDUCT.md).
@@ -19,9 +22,6 @@ to the GitHub repository.
**Before opening a new issue, please check if the issue has not been already made by searching
the [issues](https://github.com/navidrome/navidrome/issues)**
## Questions
We would like to have discussions and general queries related to Navidrome on our [Discord channel](https://discord.gg/2qMuMyHfSV).
## Pull requests
Before submitting a pull request, ensure that you go through the following:
- Open a corresponding issue for the Pull Request, if not existing. The issue can be opened following [these guidelines](#issues)

View File

@@ -9,7 +9,7 @@ GIT_SHA=source_archive
GIT_TAG=$(patsubst navidrome-%,v%,$(notdir $(PWD)))
endif
CI_RELEASER_VERSION=1.19.5-1 ## https://github.com/navidrome/ci-goreleaser
CI_RELEASER_VERSION=1.20.3-1 ## https://github.com/navidrome/ci-goreleaser
setup: check_env download-deps setup-git ##@1_Run_First Install dependencies and prepare development environment
@echo Downloading Node dependencies...
@@ -21,40 +21,59 @@ dev: check_env ##@Development Start Navidrome in development mode, with hot-re
.PHONY: dev
server: check_go_env ##@Development Start the backend in development mode
@go run github.com/cespare/reflex -d none -c reflex.conf
@go run github.com/cespare/reflex@latest -d none -c reflex.conf
.PHONY: server
watch: ##@Development Start Go tests in watch mode (re-run when code changes)
go run github.com/onsi/ginkgo/v2/ginkgo watch -notify ./...
go run github.com/onsi/ginkgo/v2/ginkgo@latest watch -notify ./...
.PHONY: watch
test: ##@Development Run Go tests
go test -race ./...
go test -race -shuffle=on ./...
.PHONY: test
testall: test ##@Development Run Go and JS tests
testall: test ##@Development Run Go and JS tests, and validate OpenAPI spec
@(cd ./ui && npm test -- --watchAll=false)
.PHONY: testall
lint: ##@Development Lint Go code
go run github.com/golangci/golangci-lint/cmd/golangci-lint run -v --timeout 5m
go run github.com/golangci/golangci-lint/cmd/golangci-lint@latest run -v --timeout 5m
.PHONY: lint
lintall: lint ##@Development Lint Go and JS code
@(cd ./ui && npm run check-formatting && npm run lint)
lintapi: api/openapi.yaml ##@Development Lint OpenAPI spec
npx @redocly/cli lint api/openapi.yaml
.PHONY: lintapi
lintall: lint lintapi ##@Development Lint Go and JS code
@(cd ./ui && npm run check-formatting) || (echo "\n\nPlease run 'npm run prettier' to fix formatting issues." && exit 1)
@(cd ./ui && npm run lint)
.PHONY: lintall
wire: check_go_env ##@Development Update Dependency Injection
go run github.com/google/wire/cmd/wire ./...
gen: check_go_env api ##@Development Update Generated Code (wire, mocks, openapi, etc)
go run github.com/google/wire/cmd/wire@latest ./...
.PHONY: wire
api: check_go_env api/openapi.yaml
go generate ./server/api/...
.PHONY: api
spec_parts=$(shell find api -name '*.yml')
api/openapi.yaml: $(spec_parts)
@echo "Bundling OpenAPI spec..."
npx @redocly/cli bundle api/spec.yml -o api/openapi.yaml
snapshots: ##@Development Update (GoLang) Snapshot tests
UPDATE_SNAPSHOTS=true go run github.com/onsi/ginkgo/v2/ginkgo ./server/subsonic/...
UPDATE_SNAPSHOTS=true go run github.com/onsi/ginkgo/v2/ginkgo@latest ./server/subsonic/...
.PHONY: snapshots
migration: ##@Development Create an empty migration file
@if [ -z "${name}" ]; then echo "Usage: make migration name=name_of_migration_file"; exit 1; fi
go run github.com/pressly/goose/cmd/goose -dir db/migration create ${name}
migration-sql: ##@Development Create an empty SQL migration file
@if [ -z "${name}" ]; then echo "Usage: make migration-sql name=name_of_migration_file"; exit 1; fi
go run github.com/pressly/goose/v3/cmd/goose@latest -dir db/migration create ${name} sql
.PHONY: migration
migration-go: ##@Development Create an empty Go migration file
@if [ -z "${name}" ]; then echo "Usage: make migration-go name=name_of_migration_file"; exit 1; fi
go run github.com/pressly/goose/v3/cmd/goose@latest -dir db/migration create ${name}
.PHONY: migration
setup-dev: setup
@@ -102,9 +121,9 @@ get-music: ##@Development Download some free music from Navidrome's demo instanc
mkdir -p music
( cd music; \
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=dev_download&id=ec2093ec4801402f1e17cc462195cdbb" > brock.zip; \
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=NavidromeUI&id=b376eeb4652d2498aa2b25ba0696725e" > back_on_earth.zip; \
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=NavidromeUI&id=e49c609b542fc51899ee8b53aa858cb4" > ugress.zip; \
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=NavidromeUI&id=350bcab3a4c1d93869e39ce496464f03" > voodoocuts.zip; \
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=dev_download&id=b376eeb4652d2498aa2b25ba0696725e" > back_on_earth.zip; \
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=dev_download&id=e49c609b542fc51899ee8b53aa858cb4" > ugress.zip; \
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=dev_download&id=350bcab3a4c1d93869e39ce496464f03" > voodoocuts.zip; \
for file in *.zip; do unzip -n $${file}; done )
@echo "Done. Remember to set your MusicFolder to ./music"
.PHONY: get-music

View File

@@ -1,2 +1,2 @@
JS: sh -c "cd ./ui && npm start"
GO: go run github.com/cespare/reflex -d none -c reflex.conf
GO: go run github.com/cespare/reflex@latest -d none -c reflex.conf

View File

@@ -1,6 +1,6 @@
<a href="https://www.navidrome.org"><img src="resources/logo-192x192.png" alt="Navidrome logo" title="navidrome" align="right" height="60px" /></a>
# Navidrome Music Server
# Navidrome Music Server &nbsp;[![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Tired%20of%20paying%20for%20music%20subscriptions%2C%20and%20not%20finding%20what%20you%20really%20like%3F%20Roll%20your%20own%20streaming%20service%21&url=https://navidrome.org&via=navidrome)
[![Last Release](https://img.shields.io/github/v/release/navidrome/navidrome?logo=github&label=latest&style=flat-square)](https://github.com/navidrome/navidrome/releases)
[![Build](https://img.shields.io/github/actions/workflow/status/navidrome/navidrome/pipeline.yml?branch=master&logo=github&style=flat-square)](https://nightly.link/navidrome/navidrome/workflows/pipeline/master)
@@ -30,11 +30,15 @@ please file a [GitHub issue](https://github.com/navidrome/navidrome/issues) or j
## Installation
See instructions in the [project's website](https://www.navidrome.org/docs/installation/)
See instructions on the [project's website](https://www.navidrome.org/docs/installation/)
If you plan to host Navidrome in the cloud, a great option is to get a virtual server at [BuyVM](https://my.frantech.ca/aff.php?aff=4605).
They have plans that start at $3.50/month! If you decide to sign up, please consider using our [affliliate link](https://my.frantech.ca/aff.php?aff=4605),
to help support the project <3
## Cloud Hosting
[PikaPods](https://www.pikapods.com) has partnered with us to offer you an
[officially supported, cloud-hosted solution](https://www.navidrome.org/docs/installation/managed/#pikapods).
A share of the revenue helps fund the development of Navidrome at no additional cost for you.
[![PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=navidrome)
## Features

28
api/parameters/_index.yml Normal file
View File

@@ -0,0 +1,28 @@
pageOffset:
$ref: './query/pageOffset.yml'
pageLimit:
$ref: './query/pageLimit.yml'
filterEquals:
$ref: './query/filterEquals.yml'
filterLessThan:
$ref: './query/filterLessThan.yml'
filterLessOrEqual:
$ref: './query/filterLessOrEqual.yml'
filterGreaterThan:
$ref: './query/filterGreaterThan.yml'
filterGreaterOrEqual:
$ref: './query/filterGreaterOrEqual.yml'
filterContains:
$ref: './query/filterContains.yml'
filterStartsWith:
$ref: './query/filterStartsWith.yml'
filterEndsWith:
$ref: './query/filterEndsWith.yml'
sort:
$ref: './query/sort.yml'
include:
$ref: './query/include.yml'
includeForTracks:
$ref: './query/includeForTracks.yml'
includeForAlbums:
$ref: './query/includeForAlbums.yml'

View File

@@ -0,0 +1,9 @@
name: filter[contains]
in: query
description: 'Filter by any property containing text. Usage: filter[contains]=property:value'
required: false
schema:
type: array
items:
type: string
pattern: '^\w+:\w+'

View File

@@ -0,0 +1,9 @@
name: filter[endsWith]
in: query
description: 'Filter by any property that ends with text. Usage: filter[endsWith]=property:value'
required: false
schema:
type: array
items:
type: string
pattern: '^\w+:\w+'

View File

@@ -0,0 +1,9 @@
name: filter[equals]
in: query
description: 'Filter by any property with an exact match. Usage: filter[equals]=property:value'
required: false
schema:
type: array
items:
type: string
pattern: '^\w+:\w+'

View File

@@ -0,0 +1,9 @@
name: filter[greaterOrEqual]
in: query
description: 'Filter by any numeric property greater than or equal to a value. Usage: filter[greaterOrEqual]=property:value'
required: false
schema:
type: array
items:
type: string
pattern: '^\w+:\d+'

View File

@@ -0,0 +1,9 @@
name: filter[greaterThan]
in: query
description: 'Filter by any numeric property greater than a value. Usage: filter[greaterThan]=property:value'
required: false
schema:
type: array
items:
type: string
pattern: '^\w+:\d+'

View File

@@ -0,0 +1,9 @@
name: filter[lessOrEqual]
in: query
description: 'Filter by any numeric property less than or equal to a value. Usage: filter[lessOrEqual]=property:value'
required: false
schema:
type: array
items:
type: string
pattern: '^\w+:\d+'

View File

@@ -0,0 +1,9 @@
name: filter[lessThan]
in: query
description: 'Filter by any numeric property less than a value. Usage: filter[lessThan]=property:value'
required: false
schema:
type: array
items:
type: string
pattern: '^\w+:\d+'

View File

@@ -0,0 +1,9 @@
name: filter[startsWith]
in: query
description: 'Filter by any property that starts with text. Usage: filter[startsWith]=property:value'
required: false
schema:
type: array
items:
type: string
pattern: '^\w+:\w+'

View File

@@ -0,0 +1,6 @@
name: include
in: query
description: Related resources to include in the response, separated by commas
required: false
schema:
type: string

View File

@@ -0,0 +1,13 @@
name: include
in: query
description: Related resources to include in the response, separated by commas
required: false
explode: false
schema:
type: array
x-go-type: includeSlice
items:
type: string
enum:
- track
- artist

View File

@@ -0,0 +1,12 @@
name: include
in: query
description: Related resources to include in the response, separated by commas
required: false
explode: false
schema:
type: array
x-go-type: includeSlice
items:
type: string
enum:
- artist

View File

@@ -0,0 +1,13 @@
name: include
in: query
description: Related resources to include in the response, separated by commas
required: false
explode: false
schema:
type: array
x-go-type: includeSlice
items:
type: string
enum:
- album
- artist

View File

@@ -0,0 +1,9 @@
name: page[limit]
in: query
description: The number of items per page
required: false
schema:
type: integer
format: int32
minimum: 0
default: 10

View File

@@ -0,0 +1,9 @@
name: page[offset]
in: query
description: The offset for pagination
required: false
schema:
type: integer
format: int32
minimum: 0
default: 0

View File

@@ -0,0 +1,6 @@
name: sort
in: query
description: Sort the results by one or more properties, separated by commas. Prefix the property with '-' for descending order.
required: false
schema:
type: string

33
api/resources/album.yml Normal file
View File

@@ -0,0 +1,33 @@
get:
summary: Retrieve an individual album
operationId: getAlbum
parameters:
- $ref: '../parameters/query/includeForAlbum.yml'
- name: albumId
in: path
description: The unique identifier of the album
required: true
schema:
type: string
responses:
'200':
description: An album object
content:
application/vnd.api+json:
schema:
type: object
required: [data]
properties:
data:
$ref: '../schemas/Album.yml'
included:
description: Included resources, as requested by the `include` query parameter
type: array
items:
$ref: '../schemas/IncludedResource.yml'
'403':
$ref: '../responses/NotAuthorized.yml'
'404':
$ref: '../responses/NotFound.yml'
'500':
$ref: '../responses/InternalServerError.yml'

44
api/resources/albums.yml Normal file
View File

@@ -0,0 +1,44 @@
get:
summary: Retrieve a list of albums
operationId: getAlbums
parameters:
- $ref: '../parameters/query/pageLimit.yml'
- $ref: '../parameters/query/pageOffset.yml'
- $ref: '../parameters/query/filterEquals.yml'
- $ref: '../parameters/query/filterContains.yml'
- $ref: '../parameters/query/filterLessThan.yml'
- $ref: '../parameters/query/filterLessOrEqual.yml'
- $ref: '../parameters/query/filterGreaterThan.yml'
- $ref: '../parameters/query/filterGreaterOrEqual.yml'
- $ref: '../parameters/query/filterStartsWith.yml'
- $ref: '../parameters/query/filterEndsWith.yml'
- $ref: '../parameters/query/sort.yml'
- $ref: '../parameters/query/includeForAlbums.yml'
responses:
'200':
description: A list of albums
content:
application/vnd.api+json:
schema:
type: object
required: [data, links]
properties:
data:
type: array
items:
$ref: '../schemas/Album.yml'
links:
$ref: '../schemas/PaginationLinks.yml'
meta:
$ref: '../schemas/PaginationMeta.yml'
included:
description: Included resources, as requested by the `include` query parameter
type: array
items:
$ref: '../schemas/IncludedResource.yml'
'400':
$ref: '../responses/BadRequest.yml'
'403':
$ref: '../responses/NotAuthorized.yml'
'500':
$ref: '../responses/InternalServerError.yml'

28
api/resources/artist.yml Normal file
View File

@@ -0,0 +1,28 @@
get:
summary: Retrieve an individual artist
operationId: getArtist
parameters:
- $ref: '../parameters/query/include.yml'
- name: artistId
in: path
description: The unique identifier of the artist
required: true
schema:
type: string
responses:
'200':
description: An artist object
content:
application/vnd.api+json:
schema:
type: object
required: [data]
properties:
data:
$ref: '../schemas/Artist.yml'
'403':
$ref: '../responses/NotAuthorized.yml'
'404':
$ref: '../responses/NotFound.yml'
'500':
$ref: '../responses/InternalServerError.yml'

39
api/resources/artists.yml Normal file
View File

@@ -0,0 +1,39 @@
get:
summary: Retrieve a list of artists
operationId: getArtists
parameters:
- $ref: '../parameters/query/pageLimit.yml'
- $ref: '../parameters/query/pageOffset.yml'
- $ref: '../parameters/query/filterEquals.yml'
- $ref: '../parameters/query/filterContains.yml'
- $ref: '../parameters/query/filterLessThan.yml'
- $ref: '../parameters/query/filterLessOrEqual.yml'
- $ref: '../parameters/query/filterGreaterThan.yml'
- $ref: '../parameters/query/filterGreaterOrEqual.yml'
- $ref: '../parameters/query/filterStartsWith.yml'
- $ref: '../parameters/query/filterEndsWith.yml'
- $ref: '../parameters/query/sort.yml'
- $ref: '../parameters/query/include.yml'
responses:
'200':
description: A list of artists
content:
application/vnd.api+json:
schema:
type: object
required: [data, links]
properties:
data:
type: array
items:
$ref: '../schemas/Artist.yml'
links:
$ref: '../schemas/PaginationLinks.yml'
meta:
$ref: '../schemas/PaginationMeta.yml'
'400':
$ref: '../responses/BadRequest.yml'
'403':
$ref: '../responses/NotAuthorized.yml'
'500':
$ref: '../responses/InternalServerError.yml'

18
api/resources/server.yml Normal file
View File

@@ -0,0 +1,18 @@
get:
summary: Get server's global info
operationId: getServerInfo
responses:
'200':
description: The responses data key maps to a resource object dictionary representing the server.
content:
application/vnd.api+json:
schema:
type: object
required: [data]
properties:
data:
$ref: '../schemas/ServerInfo.yml'
'403':
$ref: '../responses/NotAuthorized.yml'
'500':
$ref: '../responses/InternalServerError.yml'

33
api/resources/track.yml Normal file
View File

@@ -0,0 +1,33 @@
get:
summary: Retrieve an individual track
operationId: getTrack
parameters:
- $ref: '../parameters/query/includeForTracks.yml'
- name: trackId
in: path
description: The unique identifier of the track
required: true
schema:
type: string
responses:
'200':
description: A track object
content:
application/vnd.api+json:
schema:
type: object
required: [data]
properties:
data:
$ref: '../schemas/Track.yml'
included:
description: Included resources, as requested by the `include` query parameter
type: array
items:
$ref: '../schemas/IncludedResource.yml'
'403':
$ref: '../responses/NotAuthorized.yml'
'404':
$ref: '../responses/NotFound.yml'
'500':
$ref: '../responses/InternalServerError.yml'

44
api/resources/tracks.yml Normal file
View File

@@ -0,0 +1,44 @@
get:
summary: Retrieve a list of tracks
operationId: getTracks
parameters:
- $ref: '../parameters/query/pageLimit.yml'
- $ref: '../parameters/query/pageOffset.yml'
- $ref: '../parameters/query/filterEquals.yml'
- $ref: '../parameters/query/filterContains.yml'
- $ref: '../parameters/query/filterLessThan.yml'
- $ref: '../parameters/query/filterLessOrEqual.yml'
- $ref: '../parameters/query/filterGreaterThan.yml'
- $ref: '../parameters/query/filterGreaterOrEqual.yml'
- $ref: '../parameters/query/filterStartsWith.yml'
- $ref: '../parameters/query/filterEndsWith.yml'
- $ref: '../parameters/query/sort.yml'
- $ref: '../parameters/query/includeForTracks.yml'
responses:
'200':
description: A list of tracks
content:
application/vnd.api+json:
schema:
type: object
required: [data, links]
properties:
data:
type: array
items:
$ref: '../schemas/Track.yml'
links:
$ref: '../schemas/PaginationLinks.yml'
meta:
$ref: '../schemas/PaginationMeta.yml'
included:
description: Included resources, as requested by the `include` query parameter
type: array
items:
$ref: '../schemas/IncludedResource.yml'
'400':
$ref: '../responses/BadRequest.yml'
'403':
$ref: '../responses/NotAuthorized.yml'
'500':
$ref: '../responses/InternalServerError.yml'

View File

@@ -0,0 +1,5 @@
description: Bad Request
content:
application/vnd.api+json:
schema:
$ref: '../schemas/ErrorList.yml'

View File

@@ -0,0 +1,5 @@
description: Internal Server Error
content:
application/vnd.api+json:
schema:
$ref: '../schemas/ErrorList.yml'

View File

@@ -0,0 +1,5 @@
description: Not Authorized
content:
application/vnd.api+json:
schema:
$ref: '../schemas/ErrorList.yml'

View File

@@ -0,0 +1,5 @@
description: Not Found
content:
application/vnd.api+json:
schema:
$ref: '../schemas/ErrorList.yml'

8
api/responses/_index.yml Normal file
View File

@@ -0,0 +1,8 @@
NotFound:
$ref: './NotFound.yml'
NotAuthorized:
$ref: './NotAuthorized.yml'
BadRequest:
$ref: './BadRequest.yml'
InternalServerError:
$ref: './InternalServerError.yml'

20
api/schemas/Album.yml Normal file
View File

@@ -0,0 +1,20 @@
allOf:
- $ref: './ResourceObject.yml'
- type: object
properties:
attributes:
$ref: './AlbumAttributes.yml'
relationships:
type: object
properties:
artists:
type: array
items:
$ref: './AlbumArtistRelationship.yml'
tracks:
type: array
items:
$ref: './AlbumTrackRelationship.yml'
required:
- artists
- tracks

View File

@@ -0,0 +1,9 @@
type: object
properties:
meta:
$ref: './ArtistMetaObject.yml'
data:
$ref: './ResourceObject.yml'
required:
- meta
- data

View File

@@ -0,0 +1,23 @@
type: object
properties:
title:
type: string
description: The title of the album
artist:
type: string
description: The artist of the album
releaseDate:
type: string
description: The release date of the album
tracktotal:
type: integer
description: The number of tracks on the album
disctotal:
type: integer
description: The number of discs in the album
genre:
type: string
description: The genre of the album
required:
- title
- artist

View File

@@ -0,0 +1,6 @@
type: object
properties:
data:
$ref: './ResourceObject.yml'
required:
- data

27
api/schemas/Artist.yml Normal file
View File

@@ -0,0 +1,27 @@
allOf:
- $ref: './ResourceObject.yml'
- type: object
properties:
attributes:
$ref: './ArtistAttributes.yml'
relationships:
type: object
properties:
tracks:
type: object
properties:
data:
type: array
items:
$ref: './ArtistTrackRelationship.yml'
required:
- data
albums:
type: object
properties:
data:
type: array
items:
$ref: './ArtistAlbumRelationship.yml'
required:
- data

View File

@@ -0,0 +1,9 @@
type: object
properties:
meta:
$ref: './ArtistMetaObject.yml'
data:
$ref: './ResourceObject.yml'
required:
- meta
- data

View File

@@ -0,0 +1,10 @@
type: object
properties:
name:
type: string
description: The name of the artist
bio:
type: string
description: A short biography of the artist
required:
- name

View File

@@ -0,0 +1,6 @@
type: object
properties:
role:
$ref: './ArtistRole.yml'
required:
- role

View File

@@ -0,0 +1,5 @@
type: string
enum:
- artist
- albumArtist
description: The role of an artist in a track or album

View File

@@ -0,0 +1,14 @@
type: object
properties:
meta:
type: object
properties:
role:
$ref: './ArtistRole.yml'
required:
- role
data:
$ref: './ResourceObject.yml'
required:
- meta
- data

View File

@@ -0,0 +1,7 @@
type: object
required: [errors]
properties:
errors:
type: array
items:
$ref: './ErrorObject.yml'

View File

@@ -0,0 +1,10 @@
type: object
properties:
id:
type: string
status:
type: string
title:
type: string
detail:
type: string

View File

@@ -0,0 +1,10 @@
oneOf:
- $ref: './Track.yml'
- $ref: './Album.yml'
- $ref: './Artist.yml'
discriminator:
propertyName: type
mapping:
track: './Track.yml'
album: './Album.yml'
artist: './Artist.yml'

View File

@@ -0,0 +1,14 @@
type: object
properties:
first:
type: string
format: uri
prev:
type: string
format: uri
next:
type: string
format: uri
last:
type: string
format: uri

View File

@@ -0,0 +1,14 @@
type: object
properties:
currentPage:
type: integer
format: int32
description: The current page in the collection
totalPages:
type: integer
format: int32
description: The total number of pages in the collection
totalItems:
type: integer
format: int32
description: The total number of items in the collection

View File

@@ -0,0 +1,18 @@
type: object
properties:
data:
oneOf:
- $ref: './Track.yml'
- $ref: './Album.yml'
- $ref: './Artist.yml'
- type: array
items:
$ref: './ResourceObject.yml'
included:
type: array
items:
$ref: './IncludedResource.yml'
links:
$ref: './PaginationLinks.yml'
meta:
$ref: './PaginationMeta.yml'

View File

@@ -0,0 +1,8 @@
type: object
required: [id, type]
properties:
id:
type: string
description: The unique identifier for the resource
type:
$ref: './ResourceType.yml'

View File

@@ -0,0 +1,6 @@
type: string
description: The type of the resource
enum:
- album
- artist
- track

View File

@@ -0,0 +1,24 @@
type: object
required: [server, serverVersion, authRequired, features]
properties:
server:
type: string
description: The name of the server software.
example: "navidrome"
serverVersion:
type: string
description: The version number of the server.
example: "0.60.0"
authRequired:
type: boolean
description: Whether the user has access to the server.
example: true
features:
type: array
description: A list of optional features the server supports.
items:
type: string
enum:
- albums
- artists
- images

8
api/schemas/Track.yml Normal file
View File

@@ -0,0 +1,8 @@
allOf:
- $ref: './ResourceObject.yml'
- type: object
properties:
attributes:
$ref: './TrackAttributes.yml'
relationships:
$ref: './TrackRelationships.yml'

View File

@@ -0,0 +1,9 @@
type: object
properties:
meta:
$ref: './ArtistMetaObject.yml'
data:
$ref: './ResourceObject.yml'
required:
- meta
- data

View File

@@ -0,0 +1,55 @@
type: object
required: [title, artist, album, albumartist, track, mimetype, duration, channels, bitrate, size]
properties:
title:
type: string
description: The title of the track
artist: # TODO: Remove
type: string
description: The name of the artist who performed the track
albumartist: # TODO: Remove
type: string
description: The primary artist of the album the track belongs to.
album: # TODO Remove
type: string
description: The name of the album the track belongs to
genre: # TODO Remove
type: string
description: The genre of the track.
track:
type: integer
description: The track number within the album.
disc:
type: integer
description: The disc number within a multi-disc album.
year:
type: integer
description: The release year of the track or album.
bpm:
type: integer
description: The beats per minute (BPM) of the track.
recording-mbid:
type: string
description: The MusicBrainz identifier for the recording of the track.
track-mbid:
type: string
description: The MusicBrainz identifier for the track.
comments:
type: string
description: Any additional comments or notes about the track.
mimetype:
type: string
description: The MIME type of the audio file.
duration:
type: number
format: float
description: The duration of the track in seconds
channels:
type: integer
description: The number of audio channels in the track.
bitrate:
type: integer
description: The bitrate of the audio file in kilobits per second (kbps).
size:
type: integer
description: The size of the audio file in bytes.

View File

@@ -0,0 +1,12 @@
type: object
properties:
artists:
type: array
items:
$ref: './TrackArtistRelationship.yml'
albums:
type: array
items:
$ref: './AlbumTrackRelationship.yml'
required:
- artists

46
api/schemas/_index.yml Normal file
View File

@@ -0,0 +1,46 @@
ServerInfo:
$ref: './ServerInfo.yml'
ResourceObject:
$ref: './ResourceObject.yml'
ResourceType:
$ref: './ResourceType.yml'
ResourceList:
$ref: './ResourceList.yml'
IncludedResource:
$ref: './IncludedResource.yml'
Track:
$ref: './Track.yml'
TrackAttributes:
$ref: './TrackAttributes.yml'
TrackRelationships:
$ref: './TrackRelationships.yml'
TrackArtistRelationship:
$ref: './TrackArtistRelationship.yml'
ArtistRole:
$ref: './ArtistRole.yml'
Artist:
$ref: './Artist.yml'
ArtistAttributes:
$ref: './ArtistAttributes.yml'
ArtistAlbumRelationship:
$ref: './ArtistAlbumRelationship.yml'
ArtistTrackRelationship:
$ref: './ArtistTrackRelationship.yml'
ArtistMetaObject:
$ref: './ArtistMetaObject.yml'
Album:
$ref: './Album.yml'
AlbumAttributes:
$ref: './AlbumAttributes.yml'
AlbumArtistRelationship:
$ref: './AlbumArtistRelationship.yml'
AlbumTrackRelationship:
$ref: './AlbumTrackRelationship.yml'
PaginationLinks:
$ref: './PaginationLinks.yml'
PaginationMeta:
$ref: './PaginationMeta.yml'
ErrorList:
$ref: './ErrorList.yml'
ErrorObject:
$ref: './ErrorObject.yml'

52
api/spec.yml Normal file
View File

@@ -0,0 +1,52 @@
openapi: 3.0.0
info:
version: 0.2.0
title: Navidrome API
description: >
This spec describes the Navidrome API, which allows users to browse and manage their music library via a JSON:API
based interface. The API provides endpoints for albums, tracks, artists, playlists and images, along with their
relationships. Clients can retrieve information about the items in the library, filter and sort results, and
perform actions such as creating and deleting playlists. With this API, developers can build music apps and
services that integrate with Navidrome music server, providing a seamless experience for users to access and
manage their music collection.
contact:
name: Navidrome
url: https://navidrome.org
license:
name: GNU General Public License v3.0
url: https://github.com/navidrome/navidrome/blob/master/LICENSE
servers:
- url: /api/v2
security:
- bearerAuth: []
paths:
/server:
$ref: './resources/server.yml'
/tracks:
$ref: './resources/tracks.yml'
/tracks/{trackId}:
$ref: './resources/track.yml'
/artists:
$ref: './resources/artists.yml'
/artists/{artistId}:
$ref: './resources/artist.yml'
/albums:
$ref: './resources/albums.yml'
/albums/{albumId}:
$ref: './resources/album.yml'
components:
parameters:
$ref: './parameters/_index.yml'
schemas:
$ref: './schemas/_index.yml'
responses:
$ref: './responses/_index.yml'
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT

View File

@@ -8,6 +8,7 @@ import (
"strings"
"time"
"github.com/go-chi/chi/v5/middleware"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core"
@@ -60,7 +61,7 @@ func preRun() {
}
func runNavidrome() {
db.EnsureLatestVersion()
db.Init()
defer func() {
if err := db.Close(); err != nil {
log.Error("Error closing DB", err)
@@ -96,10 +97,14 @@ func startServer(ctx context.Context) func() error {
core.WriteInitialMetrics()
a.MountRouter("Prometheus metrics", conf.Server.Prometheus.MetricsPath, promhttp.Handler())
}
if conf.Server.DevEnableProfiler {
a.MountRouter("Profiling", "/debug", middleware.Profiler())
}
if strings.HasPrefix(conf.Server.UILoginBackgroundURL, "/") {
a.MountRouter("Background images", consts.DefaultUILoginBackgroundURL, backgrounds.NewHandler())
}
return a.Run(ctx, fmt.Sprintf("%s:%d", conf.Server.Address, conf.Server.Port))
a.MountRouter("New Native API", consts.URLPathAPI, CreateNewNativeAPIRouter())
return a.Run(ctx, conf.Server.Address, conf.Server.Port, conf.Server.TLSCert, conf.Server.TLSKey)
}
}
@@ -151,18 +156,23 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "configfile", "c", "", `config file (default "./navidrome.toml")`)
rootCmd.PersistentFlags().BoolVarP(&noBanner, "nobanner", "n", false, `don't show banner`)
rootCmd.PersistentFlags().String("musicfolder", viper.GetString("musicfolder"), "folder where your music is stored")
rootCmd.PersistentFlags().String("datafolder", viper.GetString("datafolder"), "folder to store application data (DB, cache...), needs write access")
rootCmd.PersistentFlags().String("datafolder", viper.GetString("datafolder"), "folder to store application data (DB), needs write access")
rootCmd.PersistentFlags().String("cachefolder", viper.GetString("cachefolder"), "folder to store cache data (transcoding, images...), needs write access")
rootCmd.PersistentFlags().StringP("loglevel", "l", viper.GetString("loglevel"), "log level, possible values: error, info, debug, trace")
_ = viper.BindPFlag("musicfolder", rootCmd.PersistentFlags().Lookup("musicfolder"))
_ = viper.BindPFlag("datafolder", rootCmd.PersistentFlags().Lookup("datafolder"))
_ = viper.BindPFlag("cachefolder", rootCmd.PersistentFlags().Lookup("cachefolder"))
_ = viper.BindPFlag("loglevel", rootCmd.PersistentFlags().Lookup("loglevel"))
rootCmd.Flags().StringP("address", "a", viper.GetString("address"), "IP address to bind")
rootCmd.Flags().IntP("port", "p", viper.GetInt("port"), "HTTP port Navidrome will use")
rootCmd.Flags().StringP("address", "a", viper.GetString("address"), "IP address to bind to")
rootCmd.Flags().IntP("port", "p", viper.GetInt("port"), "HTTP port Navidrome will listen to")
rootCmd.Flags().String("baseurl", viper.GetString("baseurl"), "base URL to configure Navidrome behind a proxy (ex: /music or http://my.server.com)")
rootCmd.Flags().String("tlscert", viper.GetString("tlscert"), "optional path to a TLS cert file (enables HTTPS listening)")
rootCmd.Flags().String("tlskey", viper.GetString("tlskey"), "optional path to a TLS key file (enables HTTPS listening)")
rootCmd.Flags().Duration("sessiontimeout", viper.GetDuration("sessiontimeout"), "how long Navidrome will wait before closing web ui idle sessions")
rootCmd.Flags().Duration("scaninterval", viper.GetDuration("scaninterval"), "how frequently to scan for changes in your music library")
rootCmd.Flags().String("baseurl", viper.GetString("baseurl"), "base URL (only the path part) to configure Navidrome behind a proxy (ex: /music)")
rootCmd.Flags().String("uiloginbackgroundurl", viper.GetString("uiloginbackgroundurl"), "URL to a backaground image used in the Login page")
rootCmd.Flags().Bool("enabletranscodingconfig", viper.GetBool("enabletranscodingconfig"), "enables transcoding configuration in the UI")
rootCmd.Flags().String("transcodingcachesize", viper.GetString("transcodingcachesize"), "size of transcoding cache")
@@ -174,9 +184,12 @@ func init() {
_ = viper.BindPFlag("address", rootCmd.Flags().Lookup("address"))
_ = viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))
_ = viper.BindPFlag("tlscert", rootCmd.Flags().Lookup("tlscert"))
_ = viper.BindPFlag("tlskey", rootCmd.Flags().Lookup("tlskey"))
_ = viper.BindPFlag("baseurl", rootCmd.Flags().Lookup("baseurl"))
_ = viper.BindPFlag("sessiontimeout", rootCmd.Flags().Lookup("sessiontimeout"))
_ = viper.BindPFlag("scaninterval", rootCmd.Flags().Lookup("scaninterval"))
_ = viper.BindPFlag("baseurl", rootCmd.Flags().Lookup("baseurl"))
_ = viper.BindPFlag("uiloginbackgroundurl", rootCmd.Flags().Lookup("uiloginbackgroundurl"))
_ = viper.BindPFlag("prometheus.enabled", rootCmd.Flags().Lookup("prometheus.enabled"))

View File

@@ -19,6 +19,7 @@ import (
"github.com/navidrome/navidrome/persistence"
"github.com/navidrome/navidrome/scanner"
"github.com/navidrome/navidrome/server"
"github.com/navidrome/navidrome/server/api"
"github.com/navidrome/navidrome/server/events"
"github.com/navidrome/navidrome/server/nativeapi"
"github.com/navidrome/navidrome/server/public"
@@ -31,16 +32,23 @@ import (
func CreateServer(musicFolder string) *server.Server {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
serverServer := server.New(dataStore)
broker := events.GetBroker()
serverServer := server.New(dataStore, broker)
return serverServer
}
func CreateNativeAPIRouter() *nativeapi.Router {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
broker := events.GetBroker()
share := core.NewShare(dataStore)
router := nativeapi.New(dataStore, broker, share)
router := nativeapi.New(dataStore, share)
return router
}
func CreateNewNativeAPIRouter() *api.Router {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
router := api.New(dataStore)
return router
}
@@ -54,13 +62,13 @@ func CreateSubsonicAPIRouter() *subsonic.Router {
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata)
transcodingCache := core.GetTranscodingCache()
mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
archiver := core.NewArchiver(mediaStreamer, dataStore)
share := core.NewShare(dataStore)
archiver := core.NewArchiver(mediaStreamer, dataStore, share)
players := core.NewPlayers(dataStore)
scanner := GetScanner()
broker := events.GetBroker()
playlists := core.NewPlaylists(dataStore)
playTracker := scrobbler.GetPlayTracker(dataStore, broker)
share := core.NewShare(dataStore)
router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, externalMetadata, scanner, broker, playlists, playTracker, share)
return router
}
@@ -76,7 +84,8 @@ func CreatePublicRouter() *public.Router {
transcodingCache := core.GetTranscodingCache()
mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
share := core.NewShare(dataStore)
router := public.New(dataStore, artworkArtwork, mediaStreamer, share)
archiver := core.NewArchiver(mediaStreamer, dataStore, share)
router := public.New(dataStore, artworkArtwork, mediaStreamer, share, archiver)
return router
}
@@ -111,7 +120,7 @@ func createScanner() scanner.Scanner {
// wire_injectors.go:
var allProviders = wire.NewSet(core.Set, artwork.Set, subsonic.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, db.Db)
var allProviders = wire.NewSet(core.Set, artwork.Set, subsonic.New, nativeapi.New, api.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, db.Db)
// Scanner must be a Singleton
var (

View File

@@ -14,6 +14,7 @@ import (
"github.com/navidrome/navidrome/persistence"
"github.com/navidrome/navidrome/scanner"
"github.com/navidrome/navidrome/server"
"github.com/navidrome/navidrome/server/api"
"github.com/navidrome/navidrome/server/events"
"github.com/navidrome/navidrome/server/nativeapi"
"github.com/navidrome/navidrome/server/public"
@@ -25,6 +26,7 @@ var allProviders = wire.NewSet(
artwork.Set,
subsonic.New,
nativeapi.New,
api.New,
public.New,
persistence.New,
lastfm.NewRouter,
@@ -46,6 +48,12 @@ func CreateNativeAPIRouter() *nativeapi.Router {
))
}
func CreateNewNativeAPIRouter() *api.Router {
panic(wire.Build(
allProviders,
))
}
func CreateSubsonicAPIRouter() *subsonic.Router {
panic(wire.Build(
allProviders,

View File

@@ -2,6 +2,7 @@ package conf
import (
"fmt"
"net/url"
"os"
"path/filepath"
"runtime"
@@ -22,12 +23,18 @@ type configOptions struct {
Port int
MusicFolder string
DataFolder string
CacheFolder string
DbPath string
LogLevel string
ScanInterval time.Duration
ScanSchedule string
SessionTimeout time.Duration
BaseURL string
BasePath string
BaseHost string
BaseScheme string
TLSCert string
TLSKey string
UILoginBackgroundURL string
UIWelcomeMessage string
MaxSidebarPlaylists int
@@ -50,11 +57,13 @@ type configOptions struct {
FFmpegPath string
CoverArtPriority string
CoverJpegQuality int
ArtistArtPriority string
EnableGravatar bool
EnableFavourites bool
EnableStarRating bool
EnableUserEditing bool
EnableSharing bool
DefaultDownloadableShare bool
DefaultTheme string
DefaultLanguage string
DefaultUIVolume int
@@ -78,6 +87,7 @@ type configOptions struct {
// DevFlags. These are used to enable/disable debugging and incomplete features
DevLogSourceLine bool
DevLogLevels map[string]string
DevEnableProfiler bool
DevAutoCreateAdminPassword string
DevAutoLoginUsername string
DevActivityPanel bool
@@ -92,8 +102,9 @@ type configOptions struct {
}
type scannerOptions struct {
Extractor string
GenreSeparators string
Extractor string
GenreSeparators string
GroupAlbumReleases bool
}
type lastfmOptions struct {
@@ -125,6 +136,11 @@ var (
func LoadFromFile(confFile string) {
viper.SetConfigFile(confFile)
err := viper.ReadInConfig()
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "FATAL: Error reading config file:", err)
os.Exit(1)
}
Load()
}
@@ -139,6 +155,16 @@ func Load() {
_, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating data path:", "path", Server.DataFolder, err)
os.Exit(1)
}
if Server.CacheFolder == "" {
Server.CacheFolder = filepath.Join(Server.DataFolder, "cache")
}
err = os.MkdirAll(Server.CacheFolder, os.ModePerm)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating cache path:", "path", Server.CacheFolder, err)
os.Exit(1)
}
Server.ConfigFile = viper.GetViper().ConfigFileUsed()
if Server.DbPath == "" {
Server.DbPath = filepath.Join(Server.DataFolder, consts.DefaultDbPath)
@@ -153,6 +179,19 @@ func Load() {
os.Exit(1)
}
if Server.BaseURL != "" {
u, err := url.Parse(Server.BaseURL)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "FATAL: Invalid BaseURL %s: %s\n", Server.BaseURL, err.Error())
os.Exit(1)
}
Server.BasePath = u.Path
u.Path = ""
u.RawQuery = ""
Server.BaseHost = u.Host
Server.BaseScheme = u.Scheme
}
// Print current configuration if log level is Debug
if log.CurrentLevel() >= log.LevelDebug {
prettyConf := pretty.Sprintf("Loaded configuration from '%s': %# v", Server.ConfigFile, Server)
@@ -219,6 +258,7 @@ func AddHook(hook func()) {
func init() {
viper.SetDefault("musicfolder", filepath.Join(".", "music"))
viper.SetDefault("cachefolder", "")
viper.SetDefault("datafolder", ".")
viper.SetDefault("loglevel", "info")
viper.SetDefault("address", "0.0.0.0")
@@ -227,6 +267,8 @@ func init() {
viper.SetDefault("scaninterval", -1)
viper.SetDefault("scanschedule", "@every 1m")
viper.SetDefault("baseurl", "")
viper.SetDefault("tlscert", "")
viper.SetDefault("tlskey", "")
viper.SetDefault("uiloginbackgroundurl", consts.DefaultUILoginBackgroundURL)
viper.SetDefault("uiwelcomemessage", "")
viper.SetDefault("maxsidebarplaylists", consts.DefaultMaxSidebarPlaylists)
@@ -249,6 +291,7 @@ func init() {
viper.SetDefault("ffmpegpath", "")
viper.SetDefault("coverartpriority", "cover.*, folder.*, front.*, embedded, external")
viper.SetDefault("coverjpegquality", 75)
viper.SetDefault("artistartpriority", "artist.*, album/artist.*, external")
viper.SetDefault("enablegravatar", false)
viper.SetDefault("enablefavourites", true)
viper.SetDefault("enablestarrating", true)
@@ -272,6 +315,7 @@ func init() {
viper.SetDefault("scanner.extractor", consts.DefaultScannerExtractor)
viper.SetDefault("scanner.genreseparators", ";/,")
viper.SetDefault("scanner.groupalbumreleases", false)
viper.SetDefault("agents", "lastfm,spotify")
viper.SetDefault("lastfm.enabled", true)
@@ -285,10 +329,12 @@ func init() {
// DevFlags. These are used to enable/disable debugging and incomplete features
viper.SetDefault("devlogsourceline", false)
viper.SetDefault("devenableprofiler", false)
viper.SetDefault("devautocreateadminpassword", "")
viper.SetDefault("devautologinusername", "")
viper.SetDefault("devactivitypanel", true)
viper.SetDefault("enablesharing", false)
viper.SetDefault("defaultdownloadableshare", false)
viper.SetDefault("devenablebufferedscrobble", true)
viper.SetDefault("devsidebarplaylists", true)
viper.SetDefault("devshowartistpage", true)

View File

@@ -32,6 +32,7 @@ const (
URLPathUI = "/app"
URLPathNativeAPI = "/api"
URLPathAPI = "/api/v2"
URLPathSubsonicAPI = "/rest"
URLPathPublic = "/share"
URLPathPublicImages = URLPathPublic + "/img"
@@ -71,10 +72,10 @@ const (
// Cache options
const (
TranscodingCacheDir = "cache/transcoding"
TranscodingCacheDir = "transcoding"
DefaultTranscodingCacheMaxItems = 0 // Unlimited
ImageCacheDir = "cache/images"
ImageCacheDir = "images"
DefaultImageCacheMaxItems = 0 // Unlimited
DefaultCacheSize = 100 * 1024 * 1024 // 100MB
@@ -94,19 +95,19 @@ var (
"name": "mp3 audio",
"targetFormat": "mp3",
"defaultBitRate": 192,
"command": "ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -f mp3 -",
"command": "ffmpeg -i %s -map 0:a:0 -b:a %bk -v 0 -f mp3 -",
},
{
"name": "opus audio",
"targetFormat": "opus",
"defaultBitRate": 128,
"command": "ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -c:a libopus -f opus -",
"command": "ffmpeg -i %s -map 0:a:0 -b:a %bk -v 0 -c:a libopus -f opus -",
},
{
"name": "aac audio",
"targetFormat": "aac",
"defaultBitRate": 256,
"command": "ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -c:a aac -f adts -",
"command": "ffmpeg -i %s -map 0:a:0 -b:a %bk -v 0 -c:a aac -f adts -",
},
}

View File

@@ -0,0 +1,7 @@
https://your.website {
reverse_proxy * navidrome:4533 {
header_up Host {http.reverse_proxy.upstream.hostport}
header_up X-Forwarded-For {http.request.remote}
header_up X-Real-IP {http.reverse_proxy.upstream.port}
}
}

View File

@@ -0,0 +1,31 @@
version: '3.6'
volumes:
caddy_data:
navidrome_data:
services:
caddy:
container_name: "caddy"
image: caddy:2.6-alpine
restart: unless-stopped
read_only: true
volumes:
- "caddy_data:/data:rw"
- "./Caddyfile:/etc/caddy/Caddyfile:ro"
ports:
- "80:80"
- "443:443"
navidrome:
container_name: "navidrome"
image: deluan/navidrome:latest
restart: unless-stopped
read_only: true
# user: 1000:1000
ports:
- "4533:4533"
volumes:
- "navidrome_data:/data"
#- "/mnt/music:/music:ro"

View File

@@ -0,0 +1,51 @@
version: "3.6"
volumes:
traefik_data:
navidrome_data:
services:
traefik:
container_name: "traefik"
image: traefik:2.9
restart: unless-stopped
read_only: true
command:
- "--log.level=ERROR"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.tc.acme.tlschallenge=true"
#- "--certificatesresolvers.tc.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
- "--certificatesresolvers.tc.acme.email=foo@foo.com"
- "--certificatesresolvers.tc.acme.storage=/letsencrypt/acme.json"
ports:
- "443:443"
volumes:
- "traefik_data:/letsencrypt"
#- "/var/run/docker.sock:/var/run/docker.sock:ro"
navidrome:
container_name: "navidrome"
image: deluan/navidrome:latest
restart: unless-stopped
read_only: true
# user: 1000:1000
ports:
- "4533:4533"
environment:
ND_SCANINTERVAL: 6h
ND_LOGLEVEL: info
ND_SESSIONTIMEOUT: 168h
ND_BASEURL: ""
volumes:
- "navidrome_data:/data"
#- "/mnt/music:/music:ro"
labels:
- "traefik.enable=true"
- "traefik.http.routers.navidrome.rule=Host(`foo.com`)"
- "traefik.http.routers.navidrome.entrypoints=websecure"
- "traefik.http.routers.navidrome.tls=true"
- "traefik.http.routers.navidrome.tls.certresolver=tc"
- "traefik.http.services.navidrome.loadbalancer.server.port=4533"

View File

@@ -0,0 +1,18 @@
version: '3.6'
volumes:
navidrome_data:
services:
navidrome:
container_name: "navidrome"
image: deluan/navidrome:latest
restart: unless-stopped
read_only: true
# user: 1000:1000
ports:
- "4533:4533"
volumes:
- "navidrome_data:/data"
#- "/mnt/music:/music:ro"

11
contrib/k8s/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Kubernetes
A couple things to keep in mind with this manifest:
1. This creates a namespace called `navidrome`. Adjust this as needed.
1. This manifest was created on [K3s](https://github.com/k3s-io/k3s), which uses its own storage provisioner called [local-path-provisioner](https://github.com/rancher/local-path-provisioner). Be sure to change the `storageClassName` of the `PersistentVolumeClaim` as needed.
1. The `PersistentVolumeClaim` sets up a 2Gi volume for Navidrome's database. Adjust this as needed.
1. Be sure to change the `image` tag from `ghcr.io/navidrome/navidrome:0.49.3` to whatever the newest version is.
1. This assumes your music is mounted on the host using `hostPath` at `/path/to/your/music/on/the/host`. Adjust this as needed.
1. The `Ingress` is already configured for `cert-manager` to obtain a Let's Encrypt TLS certificate and uses Traefik for routing. Adjust this as needed.
1. The `Ingress` presents the service at `navidrome.${SECRET_INTERNAL_DOMAIN_NAME}`, which needs to already be setup in DNS.

111
contrib/k8s/manifest.yml Normal file
View File

@@ -0,0 +1,111 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: navidrome
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: navidrome-data-pvc
namespace: navidrome
annotations:
volumeType: local
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
storageClassName: local-path
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: navidrome-deployment
namespace: navidrome
spec:
replicas: 1
revisionHistoryLimit: 2
selector:
matchLabels:
app: navidrome
template:
metadata:
labels:
app: navidrome
spec:
containers:
- name: navidrome
image: ghcr.io/navidrome/navidrome:0.49.3
ports:
- containerPort: 4533
env:
- name: ND_SCANSCHEDULE
value: "12h"
- name: ND_SESSIONTIMEOUT
value: "24h"
- name: ND_LOGLEVEL
value: "info"
- name: ND_ENABLETRANSCODINGCONFIG
value: "false"
- name: ND_TRANSCODINGCACHESIZE
value: "512MB"
- name: ND_ENABLESTARRATING
value: "false"
- name: ND_ENABLEFAVOURITES
value: "false"
volumeMounts:
- name: data
mountPath: /data
- name: music
mountPath: /music
readOnly: true
volumes:
- name: data
persistentVolumeClaim:
claimName: navidrome-data-pvc
- name: music
hostPath:
path: /path/to/your/music/on/the/host
type: Directory
---
apiVersion: v1
kind: Service
metadata:
name: navidrome-service
namespace: navidrome
spec:
type: ClusterIP
ports:
- name: http
targetPort: 4533
port: 4533
protocol: TCP
selector:
app: navidrome
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: navidrome-ingress
namespace: navidrome
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
rules:
- host: navidrome.${SECRET_INTERNAL_DOMAIN_NAME}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: navidrome-service
port:
number: 4533
tls:
- hosts:
- navidrome.${SECRET_INTERNAL_DOMAIN_NAME}
secretName: navidrome-tls

View File

@@ -38,6 +38,7 @@ RestrictNamespaces=yes
RestrictRealtime=yes
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
SystemCallFilter=setrlimit
SystemCallArchitectures=native
UMask=0066

View File

@@ -18,16 +18,18 @@ import (
type Archiver interface {
ZipAlbum(ctx context.Context, id string, format string, bitrate int, w io.Writer) error
ZipArtist(ctx context.Context, id string, format string, bitrate int, w io.Writer) error
ZipShare(ctx context.Context, id string, w io.Writer) error
ZipPlaylist(ctx context.Context, id string, format string, bitrate int, w io.Writer) error
}
func NewArchiver(ms MediaStreamer, ds model.DataStore) Archiver {
return &archiver{ds: ds, ms: ms}
func NewArchiver(ms MediaStreamer, ds model.DataStore, shares Share) Archiver {
return &archiver{ds: ds, ms: ms, shares: shares}
}
type archiver struct {
ds model.DataStore
ms MediaStreamer
ds model.DataStore
ms MediaStreamer
shares Share
}
func (a *archiver) ZipAlbum(ctx context.Context, id string, format string, bitrate int, out io.Writer) error {
@@ -69,7 +71,7 @@ func (a *archiver) zipAlbums(ctx context.Context, id string, format string, bitr
func createZipWriter(out io.Writer, format string, bitrate int) *zip.Writer {
z := zip.NewWriter(out)
comment := "Downloaded from Navidrome"
if format != "raw" {
if format != "raw" && format != "" {
comment = fmt.Sprintf("%s, transcoded to %s %dbps", comment, format, bitrate)
}
_ = z.SetComment(comment)
@@ -84,7 +86,19 @@ func (a *archiver) albumFilename(mf model.MediaFile, format string, isMultDisc b
if isMultDisc {
file = fmt.Sprintf("Disc %02d/%s", mf.DiscNumber, file)
}
return fmt.Sprintf("%s/%s", mf.Album, file)
return fmt.Sprintf("%s/%s", sanitizeName(mf.Album), file)
}
func (a *archiver) ZipShare(ctx context.Context, id string, out io.Writer) error {
s, err := a.shares.Load(ctx, id)
if !s.Downloadable {
return model.ErrNotAuthorized
}
if err != nil {
return err
}
log.Debug(ctx, "Zipping share", "name", s.ID, "format", s.Format, "bitrate", s.MaxBitRate, "numTracks", len(s.Tracks))
return a.zipMediaFiles(ctx, id, s.Format, s.MaxBitRate, out, s.Tracks)
}
func (a *archiver) ZipPlaylist(ctx context.Context, id string, format string, bitrate int, out io.Writer) error {
@@ -93,13 +107,13 @@ func (a *archiver) ZipPlaylist(ctx context.Context, id string, format string, bi
log.Error(ctx, "Error loading mediafiles from playlist", "id", id, err)
return err
}
return a.zipPlaylist(ctx, id, format, bitrate, out, pls)
}
func (a *archiver) zipPlaylist(ctx context.Context, id string, format string, bitrate int, out io.Writer, pls *model.Playlist) error {
z := createZipWriter(out, format, bitrate)
mfs := pls.MediaFiles()
log.Debug(ctx, "Zipping playlist", "name", pls.Name, "format", format, "bitrate", bitrate, "numTracks", len(mfs))
return a.zipMediaFiles(ctx, id, format, bitrate, out, mfs)
}
func (a *archiver) zipMediaFiles(ctx context.Context, id string, format string, bitrate int, out io.Writer, mfs model.MediaFiles) error {
z := createZipWriter(out, format, bitrate)
for idx, mf := range mfs {
file := a.playlistFilename(mf, format, idx)
_ = a.addFileToZip(ctx, z, mf, format, bitrate, file)
@@ -113,11 +127,14 @@ func (a *archiver) zipPlaylist(ctx context.Context, id string, format string, bi
func (a *archiver) playlistFilename(mf model.MediaFile, format string, idx int) string {
ext := mf.Suffix
if format != "raw" {
if format != "" && format != "raw" {
ext = format
}
file := fmt.Sprintf("%02d - %s - %s.%s", idx+1, mf.Artist, mf.Title, ext)
return file
return fmt.Sprintf("%02d - %s - %s.%s", idx+1, sanitizeName(mf.Artist), sanitizeName(mf.Title), ext)
}
func sanitizeName(target string) string {
return strings.ReplaceAll(target, "/", "_")
}
func (a *archiver) addFileToZip(ctx context.Context, z *zip.Writer, mf model.MediaFile, format string, bitrate int, filename string) error {
@@ -132,7 +149,7 @@ func (a *archiver) addFileToZip(ctx context.Context, z *zip.Writer, mf model.Med
}
var r io.ReadCloser
if format != "raw" {
if format != "raw" && format != "" {
r, err = a.ms.DoStream(ctx, &mf, format, bitrate)
} else {
r, err = os.Open(mf.Path)

211
core/archiver_test.go Normal file
View File

@@ -0,0 +1,211 @@
package core_test
import (
"archive/zip"
"bytes"
"context"
"io"
"strings"
"github.com/Masterminds/squirrel"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/model"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/mock"
)
var _ = Describe("Archiver", func() {
var (
arch core.Archiver
ms *mockMediaStreamer
ds *mockDataStore
sh *mockShare
)
BeforeEach(func() {
ms = &mockMediaStreamer{}
ds = &mockDataStore{}
sh = &mockShare{}
arch = core.NewArchiver(ms, ds, sh)
})
Context("ZipAlbum", func() {
It("zips an album correctly", func() {
mfs := model.MediaFiles{
{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album/Promo", DiscNumber: 1},
{Path: "test_data/02 - track2.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album/Promo", DiscNumber: 1},
}
mfRepo := &mockMediaFileRepository{}
mfRepo.On("GetAll", []model.QueryOptions{{
Filters: squirrel.Eq{"album_id": "1"},
Sort: "album",
}}).Return(mfs, nil)
ds.On("MediaFile", mock.Anything).Return(mfRepo)
ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128).Return(io.NopCloser(strings.NewReader("test")), nil).Times(3)
out := new(bytes.Buffer)
err := arch.ZipAlbum(context.Background(), "1", "mp3", 128, out)
Expect(err).To(BeNil())
zr, err := zip.NewReader(bytes.NewReader(out.Bytes()), int64(out.Len()))
Expect(err).To(BeNil())
Expect(len(zr.File)).To(Equal(2))
Expect(zr.File[0].Name).To(Equal("Album_Promo/01 - track1.mp3"))
Expect(zr.File[1].Name).To(Equal("Album_Promo/02 - track2.mp3"))
})
})
Context("ZipArtist", func() {
It("zips an artist's albums correctly", func() {
mfs := model.MediaFiles{
{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumArtistID: "1", AlbumID: "1", Album: "Album 1", DiscNumber: 1},
{Path: "test_data/02 - track2.mp3", Suffix: "mp3", AlbumArtistID: "1", AlbumID: "1", Album: "Album 1", DiscNumber: 1},
}
mfRepo := &mockMediaFileRepository{}
mfRepo.On("GetAll", []model.QueryOptions{{
Filters: squirrel.Eq{"album_artist_id": "1"},
Sort: "album",
}}).Return(mfs, nil)
ds.On("MediaFile", mock.Anything).Return(mfRepo)
ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
out := new(bytes.Buffer)
err := arch.ZipArtist(context.Background(), "1", "mp3", 128, out)
Expect(err).To(BeNil())
zr, err := zip.NewReader(bytes.NewReader(out.Bytes()), int64(out.Len()))
Expect(err).To(BeNil())
Expect(len(zr.File)).To(Equal(2))
Expect(zr.File[0].Name).To(Equal("Album 1/01 - track1.mp3"))
Expect(zr.File[1].Name).To(Equal("Album 1/02 - track2.mp3"))
})
})
Context("ZipShare", func() {
It("zips a share correctly", func() {
mfs := model.MediaFiles{
{ID: "1", Path: "test_data/01 - track1.mp3", Suffix: "mp3", Artist: "Artist 1", Title: "track1"},
{ID: "2", Path: "test_data/02 - track2.mp3", Suffix: "mp3", Artist: "Artist 2", Title: "track2"},
}
share := &model.Share{
ID: "1",
Downloadable: true,
Format: "mp3",
MaxBitRate: 128,
Tracks: mfs,
}
sh.On("Load", mock.Anything, "1").Return(share, nil)
ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
out := new(bytes.Buffer)
err := arch.ZipShare(context.Background(), "1", out)
Expect(err).To(BeNil())
zr, err := zip.NewReader(bytes.NewReader(out.Bytes()), int64(out.Len()))
Expect(err).To(BeNil())
Expect(len(zr.File)).To(Equal(2))
Expect(zr.File[0].Name).To(Equal("01 - Artist 1 - track1.mp3"))
Expect(zr.File[1].Name).To(Equal("02 - Artist 2 - track2.mp3"))
})
})
Context("ZipPlaylist", func() {
It("zips a playlist correctly", func() {
tracks := []model.PlaylistTrack{
{MediaFile: model.MediaFile{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album 1", DiscNumber: 1, Artist: "AC/DC", Title: "track1"}},
{MediaFile: model.MediaFile{Path: "test_data/02 - track2.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album 1", DiscNumber: 1, Artist: "Artist 2", Title: "track2"}},
}
pls := &model.Playlist{
ID: "1",
Name: "Test Playlist",
Tracks: tracks,
}
plRepo := &mockPlaylistRepository{}
plRepo.On("GetWithTracks", "1", true).Return(pls, nil)
ds.On("Playlist", mock.Anything).Return(plRepo)
ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
out := new(bytes.Buffer)
err := arch.ZipPlaylist(context.Background(), "1", "mp3", 128, out)
Expect(err).To(BeNil())
zr, err := zip.NewReader(bytes.NewReader(out.Bytes()), int64(out.Len()))
Expect(err).To(BeNil())
Expect(len(zr.File)).To(Equal(2))
Expect(zr.File[0].Name).To(Equal("01 - AC_DC - track1.mp3"))
Expect(zr.File[1].Name).To(Equal("02 - Artist 2 - track2.mp3"))
})
})
})
type mockDataStore struct {
mock.Mock
model.DataStore
}
func (m *mockDataStore) MediaFile(ctx context.Context) model.MediaFileRepository {
args := m.Called(ctx)
return args.Get(0).(model.MediaFileRepository)
}
func (m *mockDataStore) Playlist(ctx context.Context) model.PlaylistRepository {
args := m.Called(ctx)
return args.Get(0).(model.PlaylistRepository)
}
type mockMediaFileRepository struct {
mock.Mock
model.MediaFileRepository
}
func (m *mockMediaFileRepository) GetAll(options ...model.QueryOptions) (model.MediaFiles, error) {
args := m.Called(options)
return args.Get(0).(model.MediaFiles), args.Error(1)
}
type mockPlaylistRepository struct {
mock.Mock
model.PlaylistRepository
}
func (m *mockPlaylistRepository) GetWithTracks(id string, includeTracks bool) (*model.Playlist, error) {
args := m.Called(id, includeTracks)
return args.Get(0).(*model.Playlist), args.Error(1)
}
type mockMediaStreamer struct {
mock.Mock
core.MediaStreamer
}
func (m *mockMediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, format string, bitrate int) (*core.Stream, error) {
args := m.Called(ctx, mf, format, bitrate)
if args.Error(1) != nil {
return nil, args.Error(1)
}
return &core.Stream{ReadCloser: args.Get(0).(io.ReadCloser)}, nil
}
type mockShare struct {
mock.Mock
core.Share
}
func (m *mockShare) Load(ctx context.Context, id string) (*model.Share, error) {
args := m.Called(ctx, id)
return args.Get(0).(*model.Share), args.Error(1)
}

View File

@@ -22,7 +22,8 @@ var _ = Describe("Artwork", func() {
var ffmpeg *tests.MockFFmpeg
ctx := log.NewContext(context.TODO())
var alOnlyEmbed, alEmbedNotFound, alOnlyExternal, alExternalNotFound, alMultipleCovers model.Album
var mfWithEmbed, mfWithoutEmbed, mfCorruptedCover model.MediaFile
var arMultipleCovers model.Artist
var mfWithEmbed, mfAnotherWithEmbed, mfWithoutEmbed, mfCorruptedCover model.MediaFile
BeforeEach(func() {
DeferCleanup(configtest.SetupConfig())
@@ -30,14 +31,23 @@ var _ = Describe("Artwork", func() {
conf.Server.CoverArtPriority = "folder.*, cover.*, embedded , front.*"
ds = &tests.MockDataStore{MockedTranscoding: &tests.MockTranscodingRepo{}}
alOnlyEmbed = model.Album{ID: "222", Name: "Only embed", EmbedArtPath: "tests/fixtures/test.mp3"}
alOnlyEmbed = model.Album{ID: "222", Name: "Only embed", EmbedArtPath: "tests/fixtures/artist/an-album/test.mp3"}
alEmbedNotFound = model.Album{ID: "333", Name: "Embed not found", EmbedArtPath: "tests/fixtures/NON_EXISTENT.mp3"}
alOnlyExternal = model.Album{ID: "444", Name: "Only external", ImageFiles: "tests/fixtures/front.png"}
alOnlyExternal = model.Album{ID: "444", Name: "Only external", ImageFiles: "tests/fixtures/artist/an-album/front.png"}
alExternalNotFound = model.Album{ID: "555", Name: "External not found", ImageFiles: "tests/fixtures/NON_EXISTENT.png"}
alMultipleCovers = model.Album{ID: "666", Name: "All options", EmbedArtPath: "tests/fixtures/test.mp3",
ImageFiles: "tests/fixtures/cover.jpg" + consts.Zwsp + "tests/fixtures/front.png",
arMultipleCovers = model.Artist{ID: "777", Name: "All options"}
alMultipleCovers = model.Album{
ID: "666",
Name: "All options",
EmbedArtPath: "tests/fixtures/artist/an-album/test.mp3",
Paths: "tests/fixtures/artist/an-album",
ImageFiles: "tests/fixtures/artist/an-album/cover.jpg" + consts.Zwsp +
"tests/fixtures/artist/an-album/front.png" + consts.Zwsp +
"tests/fixtures/artist/an-album/artist.png",
AlbumArtistID: "777",
}
mfWithEmbed = model.MediaFile{ID: "22", Path: "tests/fixtures/test.mp3", HasCoverArt: true, AlbumID: "222"}
mfAnotherWithEmbed = model.MediaFile{ID: "23", Path: "tests/fixtures/artist/an-album/test.mp3", HasCoverArt: true, AlbumID: "666"}
mfWithoutEmbed = model.MediaFile{ID: "44", Path: "tests/fixtures/test.ogg", AlbumID: "444"}
mfCorruptedCover = model.MediaFile{ID: "45", Path: "tests/fixtures/test.ogg", HasCoverArt: true, AlbumID: "444"}
@@ -65,7 +75,7 @@ var _ = Describe("Artwork", func() {
Expect(err).ToNot(HaveOccurred())
_, path, err := aw.Reader(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(path).To(Equal("tests/fixtures/test.mp3"))
Expect(path).To(Equal("tests/fixtures/artist/an-album/test.mp3"))
})
It("returns ErrUnavailable if embed path is not available", func() {
ffmpeg.Error = errors.New("not available")
@@ -87,7 +97,7 @@ var _ = Describe("Artwork", func() {
Expect(err).ToNot(HaveOccurred())
_, path, err := aw.Reader(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(path).To(Equal("tests/fixtures/front.png"))
Expect(path).To(Equal("tests/fixtures/artist/an-album/front.png"))
})
It("returns ErrUnavailable if external file is not available", func() {
aw, err := newAlbumArtworkReader(ctx, aw, alExternalNotFound.CoverArtID(), nil)
@@ -111,9 +121,36 @@ var _ = Describe("Artwork", func() {
Expect(err).ToNot(HaveOccurred())
Expect(path).To(Equal(expected))
},
Entry(nil, " folder.* , cover.*,embedded,front.*", "tests/fixtures/cover.jpg"),
Entry(nil, "front.* , cover.*, embedded ,folder.*", "tests/fixtures/front.png"),
Entry(nil, " embedded , front.* , cover.*,folder.*", "tests/fixtures/test.mp3"),
Entry(nil, " folder.* , cover.*,embedded,front.*", "tests/fixtures/artist/an-album/cover.jpg"),
Entry(nil, "front.* , cover.*, embedded ,folder.*", "tests/fixtures/artist/an-album/front.png"),
Entry(nil, " embedded , front.* , cover.*,folder.*", "tests/fixtures/artist/an-album/test.mp3"),
)
})
})
Describe("artistArtworkReader", func() {
Context("Multiple covers", func() {
BeforeEach(func() {
ds.Artist(ctx).(*tests.MockArtistRepo).SetData(model.Artists{
arMultipleCovers,
})
ds.Album(ctx).(*tests.MockAlbumRepo).SetData(model.Albums{
alMultipleCovers,
})
ds.MediaFile(ctx).(*tests.MockMediaFileRepo).SetData(model.MediaFiles{
mfAnotherWithEmbed,
})
})
DescribeTable("ArtistArtPriority",
func(priority string, expected string) {
conf.Server.ArtistArtPriority = priority
aw, err := newArtistReader(ctx, aw, arMultipleCovers.CoverArtID(), nil)
Expect(err).ToNot(HaveOccurred())
_, path, err := aw.Reader(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(path).To(Equal(expected))
},
Entry(nil, " folder.* , artist.*,album/artist.*", "tests/fixtures/artist/artist.jpg"),
Entry(nil, "album/artist.*, folder.*,artist.*", "tests/fixtures/artist/an-album/artist.png"),
)
})
})

View File

@@ -79,11 +79,24 @@ func (a *artistReader) LastUpdated() time.Time {
}
func (a *artistReader) Reader(ctx context.Context) (io.ReadCloser, string, error) {
return selectImageReader(ctx, a.artID,
fromArtistFolder(ctx, a.artistFolder, "artist.*"),
fromExternalFile(ctx, a.files, "artist.*"),
fromArtistExternalSource(ctx, a.artist, a.em),
)
var ff = a.fromArtistArtPriority(ctx, conf.Server.ArtistArtPriority)
return selectImageReader(ctx, a.artID, ff...)
}
func (a *artistReader) fromArtistArtPriority(ctx context.Context, priority string) []sourceFunc {
var ff []sourceFunc
for _, pattern := range strings.Split(strings.ToLower(priority), ",") {
pattern = strings.TrimSpace(pattern)
switch {
case pattern == "external":
ff = append(ff, fromArtistExternalSource(ctx, a.artist, a.em))
case strings.HasPrefix(pattern, "album/"):
ff = append(ff, fromExternalFile(ctx, a.files, strings.TrimPrefix(pattern, "album/")))
default:
ff = append(ff, fromArtistFolder(ctx, a.artistFolder, pattern))
}
}
return ff
}
func fromArtistFolder(ctx context.Context, artistFolder string, pattern string) sourceFunc {
@@ -97,12 +110,18 @@ func fromArtistFolder(ctx context.Context, artistFolder string, pattern string)
if len(matches) == 0 {
return nil, "", fmt.Errorf(`no matches for '%s' in '%s'`, pattern, artistFolder)
}
filePath := filepath.Join(artistFolder, matches[0])
f, err := os.Open(filePath)
if err != nil {
log.Warn(ctx, "Could not open cover art file", "file", filePath, err)
return nil, "", err
for _, m := range matches {
filePath := filepath.Join(artistFolder, m)
if !model.IsImageFile(m) {
continue
}
f, err := os.Open(filePath)
if err != nil {
log.Warn(ctx, "Could not open cover art file", "file", filePath, err)
return nil, "", err
}
return f, filePath, nil
}
return f, filePath, err
return nil, "", nil
}
}

View File

@@ -129,11 +129,11 @@ func (s *Stream) EstimatedContentLength() int {
func selectTranscodingOptions(ctx context.Context, ds model.DataStore, mf *model.MediaFile, reqFormat string, reqBitRate int) (format string, bitRate int) {
format = "raw"
if reqFormat == "raw" {
return
return format, 0
}
if reqFormat == mf.Suffix && reqBitRate == 0 {
bitRate = mf.BitRate
return
return format, bitRate
}
trc, hasDefault := request.TranscodingFrom(ctx)
var cFormat string
@@ -159,7 +159,7 @@ func selectTranscodingOptions(ctx context.Context, ds model.DataStore, mf *model
cBitRate = reqBitRate
}
if cBitRate == 0 && cFormat == "" {
return
return format, bitRate
}
t, err := ds.Transcoding(ctx).FindByFormat(cFormat)
if err == nil {
@@ -184,22 +184,26 @@ var (
func GetTranscodingCache() TranscodingCache {
onceTranscodingCache.Do(func() {
instanceTranscodingCache = cache.NewFileCache("Transcoding", conf.Server.TranscodingCacheSize,
consts.TranscodingCacheDir, consts.DefaultTranscodingCacheMaxItems,
func(ctx context.Context, arg cache.Item) (io.Reader, error) {
job := arg.(*streamJob)
t, err := job.ms.ds.Transcoding(ctx).FindByFormat(job.format)
if err != nil {
log.Error(ctx, "Error loading transcoding command", "format", job.format, err)
return nil, os.ErrInvalid
}
out, err := job.ms.transcoder.Transcode(ctx, t.Command, job.mf.Path, job.bitRate)
if err != nil {
log.Error(ctx, "Error starting transcoder", "id", job.mf.ID, err)
return nil, os.ErrInvalid
}
return out, nil
})
instanceTranscodingCache = NewTranscodingCache()
})
return instanceTranscodingCache
}
func NewTranscodingCache() TranscodingCache {
return cache.NewFileCache("Transcoding", conf.Server.TranscodingCacheSize,
consts.TranscodingCacheDir, consts.DefaultTranscodingCacheMaxItems,
func(ctx context.Context, arg cache.Item) (io.Reader, error) {
job := arg.(*streamJob)
t, err := job.ms.ds.Transcoding(ctx).FindByFormat(job.format)
if err != nil {
log.Error(ctx, "Error loading transcoding command", "format", job.format, err)
return nil, os.ErrInvalid
}
out, err := job.ms.transcoder.Transcode(ctx, t.Command, job.mf.Path, job.bitRate)
if err != nil {
log.Error(ctx, "Error starting transcoder", "id", job.mf.ID, err)
return nil, os.ErrInvalid
}
return out, nil
})
}

View File

@@ -2,10 +2,8 @@ package core
import (
"context"
"os"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
@@ -16,21 +14,10 @@ import (
var _ = Describe("MediaStreamer", func() {
var ds model.DataStore
ctx := log.NewContext(context.TODO())
ctx := log.NewContext(context.Background())
BeforeEach(func() {
DeferCleanup(configtest.SetupConfig())
conf.Server.DataFolder, _ = os.MkdirTemp("", "file_caches")
conf.Server.TranscodingCacheSize = "100MB"
ds = &tests.MockDataStore{MockedTranscoding: &tests.MockTranscodingRepo{}}
ds.MediaFile(ctx).(*tests.MockMediaFileRepo).SetData(model.MediaFiles{
{ID: "123", Path: "tests/fixtures/test.mp3", Suffix: "mp3", BitRate: 128, Duration: 257.0},
})
testCache := GetTranscodingCache()
Eventually(func() bool { return testCache.Available(context.TODO()) }).Should(BeTrue())
})
AfterEach(func() {
_ = os.RemoveAll(conf.Server.DataFolder)
})
Context("selectTranscodingOptions", func() {

View File

@@ -23,18 +23,18 @@ var _ = Describe("MediaStreamer", func() {
BeforeEach(func() {
DeferCleanup(configtest.SetupConfig())
conf.Server.DataFolder, _ = os.MkdirTemp("", "file_caches")
conf.Server.CacheFolder, _ = os.MkdirTemp("", "file_caches")
conf.Server.TranscodingCacheSize = "100MB"
ds = &tests.MockDataStore{MockedTranscoding: &tests.MockTranscodingRepo{}}
ds.MediaFile(ctx).(*tests.MockMediaFileRepo).SetData(model.MediaFiles{
{ID: "123", Path: "tests/fixtures/test.mp3", Suffix: "mp3", BitRate: 128, Duration: 257.0},
})
testCache := core.GetTranscodingCache()
testCache := core.NewTranscodingCache()
Eventually(func() bool { return testCache.Available(context.TODO()) }).Should(BeTrue())
streamer = core.NewMediaStreamer(ds, ffmpeg, testCache)
})
AfterEach(func() {
_ = os.RemoveAll(conf.Server.DataFolder)
_ = os.RemoveAll(conf.Server.CacheFolder)
})
Context("NewStream", func() {

View File

@@ -35,7 +35,7 @@ func (s *shareService) Load(ctx context.Context, id string) (*model.Share, error
return nil, err
}
if !share.ExpiresAt.IsZero() && share.ExpiresAt.Before(time.Now()) {
return nil, model.ErrNotAvailable
return nil, model.ErrExpired
}
share.LastVisitedAt = time.Now()
share.VisitCount++
@@ -125,7 +125,7 @@ func (r *shareRepositoryWrapper) Save(entity interface{}) (string, error) {
}
func (r *shareRepositoryWrapper) Update(id string, entity interface{}, _ ...string) error {
cols := []string{"description"}
cols := []string{"description", "downloadable"}
// TODO Better handling of Share expiration
if !entity.(*model.Share).ExpiresAt.IsZero() {

View File

@@ -45,7 +45,7 @@ var _ = Describe("Share", func() {
entity := &model.Share{}
err := repo.Update("id", entity)
Expect(err).ToNot(HaveOccurred())
Expect(mockedRepo.(*tests.MockShareRepo).Cols).To(ConsistOf("description"))
Expect(mockedRepo.(*tests.MockShareRepo).Cols).To(ConsistOf("description", "downloadable"))
})
})
})

View File

@@ -2,6 +2,7 @@ package db
import (
"database/sql"
"embed"
"fmt"
_ "github.com/mattn/go-sqlite3"
@@ -9,7 +10,7 @@ import (
_ "github.com/navidrome/navidrome/db/migration"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/utils/singleton"
"github.com/pressly/goose"
"github.com/pressly/goose/v3"
)
var (
@@ -17,6 +18,11 @@ var (
Path string
)
//go:embed migration/*.sql
var embedMigrations embed.FS
const migrationsFolder = "migration"
func Db() *sql.DB {
return singleton.GetInstance(func() *sql.DB {
Path = conf.Server.DbPath
@@ -38,7 +44,7 @@ func Close() error {
return Db().Close()
}
func EnsureLatestVersion() {
func Init() {
db := Db()
// Disable foreign_keys to allow re-creating tables in migrations
@@ -55,18 +61,19 @@ func EnsureLatestVersion() {
gooseLogger := &logAdapter{silent: isSchemaEmpty(db)}
goose.SetLogger(gooseLogger)
goose.SetBaseFS(embedMigrations)
err = goose.SetDialect(Driver)
if err != nil {
log.Fatal("Invalid DB driver", "driver", Driver, err)
}
err = goose.Run("up", db, "./")
err = goose.Up(db, migrationsFolder)
if err != nil {
log.Fatal("Failed to apply new migrations", err)
}
}
func isSchemaEmpty(db *sql.DB) bool { // nolint:interfacer
func isSchemaEmpty(db *sql.DB) bool {
rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table' AND name='goose_db_version';") // nolint:rowserrcheck
if err != nil {
log.Fatal("Database could not be opened!", err)

View File

@@ -4,7 +4,7 @@ import (
"database/sql"
"github.com/navidrome/navidrome/log"
"github.com/pressly/goose"
"github.com/pressly/goose/v3"
)
func init() {

View File

@@ -3,7 +3,7 @@ package migrations
import (
"database/sql"
"github.com/pressly/goose"
"github.com/pressly/goose/v3"
)
func init() {

View File

@@ -3,7 +3,7 @@ package migrations
import (
"database/sql"
"github.com/pressly/goose"
"github.com/pressly/goose/v3"
)
func init() {

View File

@@ -3,7 +3,7 @@ package migrations
import (
"database/sql"
"github.com/pressly/goose"
"github.com/pressly/goose/v3"
)
func init() {

View File

@@ -3,7 +3,7 @@ package migrations
import (
"database/sql"
"github.com/pressly/goose"
"github.com/pressly/goose/v3"
)
func init() {

View File

@@ -3,7 +3,7 @@ package migrations
import (
"database/sql"
"github.com/pressly/goose"
"github.com/pressly/goose/v3"
)
func init() {

View File

@@ -3,7 +3,7 @@ package migrations
import (
"database/sql"
"github.com/pressly/goose"
"github.com/pressly/goose/v3"
)
func init() {

View File

@@ -3,7 +3,7 @@ package migrations
import (
"database/sql"
"github.com/pressly/goose"
"github.com/pressly/goose/v3"
)
func init() {

Some files were not shown because too many files have changed in this diff Show More