Compare commits

..

144 Commits

Author SHA1 Message Date
Deluan
af4b2bb4c9 go mod tidy 2023-05-13 21:30:05 -04:00
Deluan
4773adba00 Add aliases for playlists and service commands 2023-05-13 21:28:13 -04:00
Deluan
7bbf4cbaea Fix lint errors 2023-05-13 21:28:13 -04:00
Deluan
cff19445ba Add service management 2023-05-13 21:28:11 -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
Kendall Garner
8b93962fad Limit share size while handling theme properly (#2171)
* limit player to 768 px

Signed-off-by: Kendall Garner <17521368+kgarner7@users.noreply.github.com>

* fix size limitation

---------

Signed-off-by: Kendall Garner <17521368+kgarner7@users.noreply.github.com>
2023-02-13 20:00:39 -05:00
Kendall Garner
b129cae0d8 Only create context if gain mode active (#2173) 2023-02-13 19:57:23 -05:00
Deluan
2400e4f60d Fix DB migration. Fix #2168 2023-02-12 14:58:33 -05:00
Deluan Quintão
3cd934abd7 Update translations (#2159)
Co-authored-by: deluan <deluan@users.noreply.github.com>
2023-02-11 20:25:01 -05:00
Deluan
727632b616 Refactor play tracking 2023-02-11 18:52:28 -05:00
Kendall Garner
9e268678f2 Limit Share player to 768 px (#2164)
Signed-off-by: Kendall Garner <17521368+kgarner7@users.noreply.github.com>
2023-02-11 12:38:35 -05:00
RTapeLoadingError
bb29ad3b12 Update Spanish translation (#2165)
Updated some empty fields.
2023-02-11 12:33:59 -05:00
Deluan
b68ed2e4f9 Fix album's image_files 2023-02-09 18:29:08 -05:00
Deluan
0c3ac906b8 Enable ReplayGain by default and always import RG tags 2023-02-09 17:45:38 -05:00
Deluan
b0e58cb885 Use Navidrome's own public images endpoint for getAlbumInfo's imageURLs 2023-02-08 20:03:31 -05:00
Deluan
806713719f Add lastUpdated to coverArt ids. Helps with invalidating art cache client-side. 2023-02-08 20:03:31 -05:00
Deluan
a3b8682d44 Fix polling of buffered scrobbles 2023-02-07 19:18:26 -05:00
Deluan
0bbb54934b Use Go 1.20 in pipeline, drop support for 1.18 2023-02-07 14:28:02 -05:00
Deluan
759ff844e2 Make ffmpeg path configurable, also finds it automatically in current folder. Fixes #1932 2023-02-07 13:46:09 -05:00
Deluan
b8c5e49dd3 Close stream when downloading files, fix fd leak 2023-02-07 09:58:50 -05:00
Deluan
05c6cdea1a Don't cancel transcoding session if context is canceled 2023-02-07 09:58:50 -05:00
Daniel Hammer
fc8462dc8a "Spell-Jacking" mitigation ~ prevent sensitive data leak from spell checker. (#2091)
@see https://www.otto-js.com/news/article/chrome-and-edge-enhanced-spellcheck-features-expose-pii-even-your-passwords

Co-authored-by: Daniel Hammer <daniel.hammer+oss@gmail.com>
2023-02-06 16:29:28 -05:00
Deluan
9d459fbd0a Abort start-up if config file is invalid 2023-02-06 13:00:07 -05:00
Deluan
9b2dd1bb06 Fix playlist delete and reorder actions 2023-02-06 10:41:33 -05:00
Deluan
bfaf4a3388 Add logs to cache hunter 2023-02-06 10:41:33 -05:00
Deluan
a7f15facf9 Bump github.com/golangci/golangci-lint to 1.51.1 2023-02-06 10:41:33 -05:00
Deluan
ee8f6447eb Add option to disable Cache Warmer. Related to #2142 2023-02-06 10:41:33 -05:00
Deluan
dad4949a6d Refactor Subsonic search to make it a bit more readable 2023-02-05 00:58:34 -05:00
Deluan
3ce3185118 Don't retrieve Various Artists and Unknown Artist info from Last.fm 2023-02-04 21:18:51 -05:00
Deluan
a50d9c8b67 Use the latest sanitize, to fix some diacritics 2023-02-04 19:09:14 -05:00
311 changed files with 4386 additions and 1964 deletions

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:
@@ -16,10 +16,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,7 +39,7 @@ 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
@@ -48,7 +48,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go_version: [1.18.x,1.19.x]
go_version: [1.20.x,1.19.x]
steps:
- name: Install taglib
run: sudo apt-get install libtag1-dev
@@ -69,7 +69,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
@@ -126,7 +126,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 +134,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 +142,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:
@@ -158,7 +158,7 @@ jobs:
retention-days: 7
docker:
name: Build Docker images
name: Build and publish Docker images
needs: [binaries]
runs-on: ubuntu-latest
env:
@@ -183,11 +183,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:
@@ -14,6 +14,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:

4
.gitignore vendored
View File

@@ -14,12 +14,14 @@ 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

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,15 +21,15 @@ 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
@@ -37,24 +37,30 @@ testall: test ##@Development Run Go and JS tests
.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)
@(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 ./...
go run github.com/google/wire/cmd/wire@latest ./...
.PHONY: wire
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

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

View File

@@ -27,9 +27,10 @@ func init() {
}
var plsCmd = &cobra.Command{
Use: "pls",
Short: "Export playlists",
Long: "Export Navidrome playlists to M3U files",
Use: "playlists",
Aliases: []string{"pls", "playlist"},
Short: "Export playlists",
Long: "Export Navidrome playlists to M3U files",
Run: func(cmd *cobra.Command, args []string) {
runExporter()
},

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"
@@ -38,7 +39,7 @@ Complete documentation is available at https://www.navidrome.org/docs`,
preRun()
},
Run: func(cmd *cobra.Command, args []string) {
runNavidrome()
runNavidrome(context.Background())
},
Version: consts.Version,
}
@@ -59,8 +60,8 @@ func preRun() {
conf.Load()
}
func runNavidrome() {
db.EnsureLatestVersion()
func runNavidrome(ctx context.Context) {
db.Init()
defer func() {
if err := db.Close(); err != nil {
log.Error("Error closing DB", err)
@@ -68,7 +69,7 @@ func runNavidrome() {
log.Info("Navidrome stopped, bye.")
}()
g, ctx := errgroup.WithContext(context.Background())
g, ctx := errgroup.WithContext(ctx)
g.Go(startServer(ctx))
g.Go(startSignaler(ctx))
g.Go(startScheduler(ctx))
@@ -96,10 +97,13 @@ 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))
return a.Run(ctx, conf.Server.Address, conf.Server.Port, conf.Server.TLSCert, conf.Server.TLSKey)
}
}
@@ -158,11 +162,14 @@ func init() {
_ = viper.BindPFlag("datafolder", rootCmd.PersistentFlags().Lookup("datafolder"))
_ = 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 +181,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"))

191
cmd/svc.go Normal file
View File

@@ -0,0 +1,191 @@
package cmd
import (
"context"
"fmt"
"os"
"path/filepath"
"sync"
"github.com/kardianos/service"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/spf13/cobra"
)
var (
svcStatusLabels = map[service.Status]string{
service.StatusUnknown: "Unknown",
service.StatusStopped: "Stopped",
service.StatusRunning: "Running",
}
)
func init() {
svcCmd.AddCommand(buildInstallCmd())
svcCmd.AddCommand(buildUninstallCmd())
svcCmd.AddCommand(buildStartCmd())
svcCmd.AddCommand(buildStopCmd())
svcCmd.AddCommand(buildStatusCmd())
rootCmd.AddCommand(svcCmd)
}
var svcCmd = &cobra.Command{
Use: "service",
Aliases: []string{"svc"},
Short: "Manage Navidrome as a service",
Long: fmt.Sprintf("Manage Navidrome as a service, using the OS service manager (%s)", service.Platform()),
Run: runServiceCmd,
}
type svcControl struct {
ctx context.Context
cancel context.CancelFunc
}
func (p *svcControl) Start(_ service.Service) error {
p.ctx, p.cancel = context.WithCancel(context.Background())
go p.run()
return nil
}
func (p *svcControl) run() {
runNavidrome(p.ctx)
}
func (p *svcControl) Stop(_ service.Service) error {
log.Info("Stopping service")
p.cancel()
return nil
}
var (
svc service.Service
svcOnce = sync.Once{}
)
func svcInstance() service.Service {
svcOnce.Do(func() {
options := make(service.KeyValue)
options["Restart"] = "on-success"
options["SuccessExitStatus"] = "1 2 8 SIGKILL"
options["UserService"] = true
options["LogDirectory"] = conf.Server.DataFolder
svcConfig := &service.Config{
Name: "Navidrome",
DisplayName: "Navidrome",
Description: "Navidrome is a self-hosted music server and streamer",
Dependencies: []string{
"Requires=network.target",
"After=network-online.target syslog.target"},
WorkingDirectory: executablePath(),
Option: options,
}
if conf.Server.ConfigFile != "" {
svcConfig.Arguments = []string{"-c", conf.Server.ConfigFile}
}
prg := &svcControl{}
var err error
svc, err = service.New(prg, svcConfig)
if err != nil {
log.Fatal(err)
}
})
return svc
}
func runServiceCmd(cmd *cobra.Command, _ []string) {
_ = cmd.Help()
}
func executablePath() string {
ex, err := os.Executable()
if err != nil {
log.Fatal(err)
}
return filepath.Dir(ex)
}
func buildInstallCmd() *cobra.Command {
runInstallCmd := func(_ *cobra.Command, _ []string) {
var err error
println("Installing service with:")
println(" working directory: " + executablePath())
println(" music folder: " + conf.Server.MusicFolder)
println(" data folder: " + conf.Server.DataFolder)
if cfgFile != "" {
conf.Server.ConfigFile, err = filepath.Abs(cfgFile)
if err != nil {
log.Fatal(err)
}
println(" config file: " + conf.Server.ConfigFile)
}
err = svcInstance().Install()
if err != nil {
log.Fatal(err)
}
println("Service installed. Use 'navidrome svc start' to start it.")
}
return &cobra.Command{
Use: "install",
Short: "Install Navidrome service.",
Run: runInstallCmd,
}
}
func buildUninstallCmd() *cobra.Command {
return &cobra.Command{
Use: "uninstall",
Short: "Uninstall Navidrome service. Does not delete the music or data folders",
Run: func(cmd *cobra.Command, args []string) {
err := svcInstance().Uninstall()
if err != nil {
log.Fatal(err)
}
println("Service uninstalled. Music and data folders are still intact.")
},
}
}
func buildStartCmd() *cobra.Command {
return &cobra.Command{
Use: "start",
Short: "Start Navidrome service",
Run: func(cmd *cobra.Command, args []string) {
err := svcInstance().Start()
if err != nil {
log.Fatal(err)
}
println("Service started. Use 'navidrome svc status' to check its status.")
},
}
}
func buildStopCmd() *cobra.Command {
return &cobra.Command{
Use: "stop",
Short: "Stop Navidrome service",
Run: func(cmd *cobra.Command, args []string) {
err := svcInstance().Stop()
if err != nil {
log.Fatal(err)
}
println("Service stopped. Use 'navidrome svc status' to check its status.")
},
}
}
func buildStatusCmd() *cobra.Command {
return &cobra.Command{
Use: "status",
Short: "Show Navidrome service status",
Run: func(cmd *cobra.Command, args []string) {
status, err := svcInstance().Status()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Navidrome is %s.\n", svcStatusLabels[status])
},
}
}

View File

@@ -31,16 +31,16 @@ 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
}
@@ -54,13 +54,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 +76,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
}

View File

@@ -2,6 +2,7 @@ package conf
import (
"fmt"
"net/url"
"os"
"path/filepath"
"runtime"
@@ -28,6 +29,11 @@ type configOptions struct {
ScanSchedule string
SessionTimeout time.Duration
BaseURL string
BasePath string
BaseHost string
BaseScheme string
TLSCert string
TLSKey string
UILoginBackgroundURL string
UIWelcomeMessage string
MaxSidebarPlaylists int
@@ -37,6 +43,7 @@ type configOptions struct {
EnableMediaFileCoverArt bool
TranscodingCacheSize string
ImageCacheSize string
EnableArtworkPrecache bool
AutoImportPlaylists bool
PlaylistsPath string
AutoTranscodeDownload bool
@@ -46,14 +53,16 @@ type configOptions struct {
IgnoredArticles string
IndexGroups string
SubsonicArtistParticipations bool
ProbeCommand string
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
@@ -77,6 +86,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
@@ -152,6 +162,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)
@@ -226,12 +249,15 @@ 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)
viper.SetDefault("enabletranscodingconfig", false)
viper.SetDefault("transcodingcachesize", "100MB")
viper.SetDefault("imagecachesize", "100MB")
viper.SetDefault("enableartworkprecache", true)
viper.SetDefault("autoimportplaylists", true)
viper.SetDefault("playlistspath", consts.DefaultPlaylistsPath)
viper.SetDefault("enabledownloads", true)
@@ -244,9 +270,10 @@ func init() {
viper.SetDefault("ignoredarticles", "The El La Los Las Le Les Os As O A")
viper.SetDefault("indexgroups", "A B C D E F G H I J K L M N O P Q R S T U V W X-Z(XYZ) [Unknown]([)")
viper.SetDefault("subsonicartistparticipations", false)
viper.SetDefault("probecommand", "ffmpeg %s -f ffmetadata")
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)
@@ -254,7 +281,7 @@ func init() {
viper.SetDefault("defaulttheme", "Dark")
viper.SetDefault("defaultlanguage", "")
viper.SetDefault("defaultuivolume", consts.DefaultUIVolume)
viper.SetDefault("enablereplaygain", false)
viper.SetDefault("enablereplaygain", true)
viper.SetDefault("enablecoveranimation", true)
viper.SetDefault("gatrackingid", "")
viper.SetDefault("enablelogredacting", true)
@@ -283,10 +310,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)
@@ -317,6 +346,7 @@ func InitConfig(cfgFile string) {
err := viper.ReadInConfig()
if viper.ConfigFileUsed() != "" && err != nil {
_, _ = fmt.Fprintln(os.Stderr, "FATAL: Navidrome could not open config file: ", err)
os.Exit(1)
}
}

View File

@@ -116,6 +116,7 @@ var (
var (
VariousArtists = "Various Artists"
VariousArtistsID = fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(VariousArtists))))
UnknownAlbum = "[Unknown Album]"
UnknownArtist = "[Unknown Artist]"
UnknownArtistID = fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(UnknownArtist))))
VariousArtistsMbzId = "89ad4ac3-39f7-470e-963a-56509c546377"

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

@@ -42,8 +42,11 @@ func (a *Agents) AgentName() string {
}
func (a *Agents) GetArtistMBID(ctx context.Context, id string, name string) (string, error) {
if id == consts.UnknownArtistID {
switch id {
case consts.UnknownArtistID:
return "", ErrNotFound
case consts.VariousArtistsID:
return "", nil
}
start := time.Now()
for _, ag := range a.agents {
@@ -64,8 +67,11 @@ func (a *Agents) GetArtistMBID(ctx context.Context, id string, name string) (str
}
func (a *Agents) GetArtistURL(ctx context.Context, id, name, mbid string) (string, error) {
if id == consts.UnknownArtistID {
switch id {
case consts.UnknownArtistID:
return "", ErrNotFound
case consts.VariousArtistsID:
return "", nil
}
start := time.Now()
for _, ag := range a.agents {
@@ -86,8 +92,11 @@ func (a *Agents) GetArtistURL(ctx context.Context, id, name, mbid string) (strin
}
func (a *Agents) GetArtistBiography(ctx context.Context, id, name, mbid string) (string, error) {
if id == consts.UnknownArtistID {
switch id {
case consts.UnknownArtistID:
return "", ErrNotFound
case consts.VariousArtistsID:
return "", nil
}
start := time.Now()
for _, ag := range a.agents {
@@ -99,7 +108,7 @@ func (a *Agents) GetArtistBiography(ctx context.Context, id, name, mbid string)
continue
}
bio, err := agent.GetArtistBiography(ctx, id, name, mbid)
if bio != "" && err == nil {
if err == nil {
log.Debug(ctx, "Got Biography", "agent", ag.AgentName(), "artist", name, "len", len(bio), "elapsed", time.Since(start))
return bio, nil
}
@@ -108,8 +117,11 @@ func (a *Agents) GetArtistBiography(ctx context.Context, id, name, mbid string)
}
func (a *Agents) GetSimilarArtists(ctx context.Context, id, name, mbid string, limit int) ([]Artist, error) {
if id == consts.UnknownArtistID {
switch id {
case consts.UnknownArtistID:
return nil, ErrNotFound
case consts.VariousArtistsID:
return nil, nil
}
start := time.Now()
for _, ag := range a.agents {
@@ -134,8 +146,11 @@ func (a *Agents) GetSimilarArtists(ctx context.Context, id, name, mbid string, l
}
func (a *Agents) GetArtistImages(ctx context.Context, id, name, mbid string) ([]ExternalImage, error) {
if id == consts.UnknownArtistID {
switch id {
case consts.UnknownArtistID:
return nil, ErrNotFound
case consts.VariousArtistsID:
return nil, nil
}
start := time.Now()
for _, ag := range a.agents {
@@ -156,8 +171,11 @@ func (a *Agents) GetArtistImages(ctx context.Context, id, name, mbid string) ([]
}
func (a *Agents) GetArtistTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]Song, error) {
if id == consts.UnknownArtistID {
switch id {
case consts.UnknownArtistID:
return nil, ErrNotFound
case consts.VariousArtistsID:
return nil, nil
}
start := time.Now()
for _, ag := range a.agents {
@@ -178,6 +196,9 @@ func (a *Agents) GetArtistTopSongs(ctx context.Context, id, artistName, mbid str
}
func (a *Agents) GetAlbumInfo(ctx context.Context, name, artist, mbid string) (*AlbumInfo, error) {
if name == consts.UnknownAlbum {
return nil, ErrNotFound
}
start := time.Now()
for _, ag := range a.agents {
if utils.IsCtxDone(ctx) {

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/tests"
@@ -61,6 +62,18 @@ var _ = Describe("Agents", func() {
Expect(ag.GetArtistMBID(ctx, "123", "test")).To(Equal("mbid"))
Expect(mock.Args).To(ConsistOf("123", "test"))
})
It("returns empty if artist is Various Artists", func() {
mbid, err := ag.GetArtistMBID(ctx, consts.VariousArtistsID, consts.VariousArtists)
Expect(err).ToNot(HaveOccurred())
Expect(mbid).To(BeEmpty())
Expect(mock.Args).To(BeEmpty())
})
It("returns not found if artist is Unknown Artist", func() {
mbid, err := ag.GetArtistMBID(ctx, consts.VariousArtistsID, consts.VariousArtists)
Expect(err).ToNot(HaveOccurred())
Expect(mbid).To(BeEmpty())
Expect(mock.Args).To(BeEmpty())
})
It("skips the agent if it returns an error", func() {
mock.Err = errors.New("error")
_, err := ag.GetArtistMBID(ctx, "123", "test")
@@ -80,6 +93,18 @@ var _ = Describe("Agents", func() {
Expect(ag.GetArtistURL(ctx, "123", "test", "mb123")).To(Equal("url"))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
})
It("returns empty if artist is Various Artists", func() {
url, err := ag.GetArtistURL(ctx, consts.VariousArtistsID, consts.VariousArtists, "")
Expect(err).ToNot(HaveOccurred())
Expect(url).To(BeEmpty())
Expect(mock.Args).To(BeEmpty())
})
It("returns not found if artist is Unknown Artist", func() {
url, err := ag.GetArtistURL(ctx, consts.VariousArtistsID, consts.VariousArtists, "")
Expect(err).ToNot(HaveOccurred())
Expect(url).To(BeEmpty())
Expect(mock.Args).To(BeEmpty())
})
It("skips the agent if it returns an error", func() {
mock.Err = errors.New("error")
_, err := ag.GetArtistURL(ctx, "123", "test", "mb123")
@@ -99,6 +124,18 @@ var _ = Describe("Agents", func() {
Expect(ag.GetArtistBiography(ctx, "123", "test", "mb123")).To(Equal("bio"))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
})
It("returns empty if artist is Various Artists", func() {
bio, err := ag.GetArtistBiography(ctx, consts.VariousArtistsID, consts.VariousArtists, "")
Expect(err).ToNot(HaveOccurred())
Expect(bio).To(BeEmpty())
Expect(mock.Args).To(BeEmpty())
})
It("returns not found if artist is Unknown Artist", func() {
bio, err := ag.GetArtistBiography(ctx, consts.VariousArtistsID, consts.VariousArtists, "")
Expect(err).ToNot(HaveOccurred())
Expect(bio).To(BeEmpty())
Expect(mock.Args).To(BeEmpty())
})
It("skips the agent if it returns an error", func() {
mock.Err = errors.New("error")
_, err := ag.GetArtistBiography(ctx, "123", "test", "mb123")

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"regexp"
"strconv"
"strings"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
@@ -21,6 +22,11 @@ const (
sessionKeyProperty = "LastFMSessionKey"
)
var ignoredBiographies = []string{
// Unknown Artist
`<a href="https://www.last.fm/music/`,
}
type lastfmAgent struct {
ds model.DataStore
sessionKeys *agents.SessionKeys
@@ -124,9 +130,15 @@ func (l *lastfmAgent) GetArtistBiography(ctx context.Context, id, name, mbid str
if err != nil {
return "", err
}
a.Bio.Summary = strings.TrimSpace(a.Bio.Summary)
if a.Bio.Summary == "" {
return "", agents.ErrNotFound
}
for _, ign := range ignoredBiographies {
if strings.HasPrefix(a.Bio.Summary, ign) {
return "", nil
}
}
return a.Bio.Summary, nil
}

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)
@@ -87,19 +89,31 @@ func (a *archiver) albumFilename(mf model.MediaFile, format string, isMultDisc b
return fmt.Sprintf("%s/%s", 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 {
pls, err := a.ds.Playlist(ctx).GetWithTracks(id, true)
if err != nil {
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,7 +127,7 @@ 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)
@@ -132,7 +146,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 1", DiscNumber: 1},
{Path: "test_data/02 - track2.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album 1", 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(2)
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 1/01 - track1.mp3"))
Expect(zr.File[1].Name).To(Equal("Album 1/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: "Artist 1", 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 - Artist 1 - 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

@@ -73,6 +73,10 @@ func (a *artwork) Get(ctx context.Context, artID model.ArtworkID, size int) (rea
return r, artReader.LastUpdated(), nil
}
type coverArtGetter interface {
CoverArtID() model.ArtworkID
}
func (a *artwork) getArtworkId(ctx context.Context, id string) (model.ArtworkID, error) {
if id == "" {
return model.ArtworkID{}, ErrUnavailable
@@ -87,18 +91,17 @@ func (a *artwork) getArtworkId(ctx context.Context, id string) (model.ArtworkID,
if err != nil {
return model.ArtworkID{}, err
}
if e, ok := entity.(coverArtGetter); ok {
artID = e.CoverArtID()
}
switch e := entity.(type) {
case *model.Artist:
artID = model.NewArtworkID(model.KindArtistArtwork, e.ID)
log.Trace(ctx, "ID is for an Artist", "id", id, "name", e.Name, "artist", e.Name)
case *model.Album:
artID = model.NewArtworkID(model.KindAlbumArtwork, e.ID)
log.Trace(ctx, "ID is for an Album", "id", id, "name", e.Name, "artist", e.AlbumArtist)
case *model.MediaFile:
artID = model.NewArtworkID(model.KindMediaFileArtwork, e.ID)
log.Trace(ctx, "ID is for a MediaFile", "id", id, "title", e.Title, "album", e.Album)
case *model.Playlist:
artID = model.NewArtworkID(model.KindPlaylistArtwork, e.ID)
log.Trace(ctx, "ID is for a Playlist", "id", id, "name", e.Name)
}
return artID, nil

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"}
@@ -49,7 +59,7 @@ var _ = Describe("Artwork", func() {
Describe("albumArtworkReader", func() {
Context("ID not found", func() {
It("returns ErrNotFound if album is not in the DB", func() {
_, err := newAlbumArtworkReader(ctx, aw, model.MustParseArtworkID("al-NOT_FOUND"), nil)
_, err := newAlbumArtworkReader(ctx, aw, model.MustParseArtworkID("al-NOT-FOUND"), nil)
Expect(err).To(MatchError(model.ErrNotFound))
})
})
@@ -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"),
)
})
})
@@ -157,14 +194,14 @@ var _ = Describe("Artwork", func() {
Expect(err).ToNot(HaveOccurred())
_, path, err := aw.Reader(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(path).To(Equal("al-444"))
Expect(path).To(Equal("al-444_0"))
})
It("returns album cover if media file has no cover art", func() {
aw, err := newMediafileArtworkReader(ctx, aw, model.MustParseArtworkID("mf-"+mfWithoutEmbed.ID))
Expect(err).ToNot(HaveOccurred())
_, path, err := aw.Reader(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(path).To(Equal("al-444"))
Expect(path).To(Equal("al-444_0"))
})
})
})

View File

@@ -23,7 +23,7 @@ type CacheWarmer interface {
func NewCacheWarmer(artwork Artwork, cache cache.FileCache) CacheWarmer {
// If image cache is disabled, return a NOOP implementation
if conf.Server.ImageCacheSize == "0" {
if conf.Server.ImageCacheSize == "0" || !conf.Server.EnableArtworkPrecache {
return &noopCacheWarmer{}
}

View File

@@ -20,8 +20,9 @@ type cacheKey struct {
func (k *cacheKey) Key() string {
return fmt.Sprintf(
"%s.%d",
k.artID,
"%s-%s.%d",
k.artID.Kind,
k.artID.ID,
k.lastUpdate.UnixMilli(),
)
}

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

@@ -425,7 +425,7 @@ func (e *externalMetadata) findMatchingTrack(ctx context.Context, mbid string, a
func (e *externalMetadata) callGetURL(ctx context.Context, agent agents.ArtistURLRetriever, artist *auxArtist) {
artisURL, err := agent.GetArtistURL(ctx, artist.ID, artist.Name, artist.MbzArtistID)
if artisURL == "" || err != nil {
if err != nil {
return
}
artist.ExternalUrl = artisURL
@@ -433,7 +433,7 @@ func (e *externalMetadata) callGetURL(ctx context.Context, agent agents.ArtistUR
func (e *externalMetadata) callGetBiography(ctx context.Context, agent agents.ArtistBiographyRetriever, artist *auxArtist) {
bio, err := agent.GetArtistBiography(ctx, artist.ID, clearName(artist.Name), artist.MbzArtistID)
if bio == "" || err != nil {
if err != nil {
return
}
bio = utils.SanitizeText(bio)
@@ -443,7 +443,7 @@ func (e *externalMetadata) callGetBiography(ctx context.Context, agent agents.Ar
func (e *externalMetadata) callGetImage(ctx context.Context, agent agents.ArtistImageRetriever, artist *auxArtist) {
images, err := agent.GetArtistImages(ctx, artist.ID, artist.Name, artist.MbzArtistID)
if len(images) == 0 || err != nil {
if err != nil {
return
}
sort.Slice(images, func(i, j int) bool { return images[i].Size > images[j].Size })

View File

@@ -9,37 +9,64 @@ import (
"os/exec"
"strconv"
"strings"
"sync"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
)
type FFmpeg interface {
Transcode(ctx context.Context, command, path string, maxBitRate int) (io.ReadCloser, error)
ExtractImage(ctx context.Context, path string) (io.ReadCloser, error)
// TODO Move scanner ffmpeg probe to here
Probe(ctx context.Context, files []string) (string, error)
CmdPath() (string, error)
}
func New() FFmpeg {
return &ffmpeg{}
}
const extractImageCmd = "ffmpeg -i %s -an -vcodec copy -f image2pipe -"
const (
extractImageCmd = "ffmpeg -i %s -an -vcodec copy -f image2pipe -"
probeCmd = "ffmpeg %s -f ffmetadata"
)
type ffmpeg struct{}
func (e *ffmpeg) Transcode(ctx context.Context, command, path string, maxBitRate int) (io.ReadCloser, error) {
if _, err := ffmpegCmd(); err != nil {
return nil, err
}
args := createFFmpegCommand(command, path, maxBitRate)
return e.start(ctx, args)
}
func (e *ffmpeg) ExtractImage(ctx context.Context, path string) (io.ReadCloser, error) {
if _, err := ffmpegCmd(); err != nil {
return nil, err
}
args := createFFmpegCommand(extractImageCmd, path, 0)
return e.start(ctx, args)
}
func (e *ffmpeg) Probe(ctx context.Context, files []string) (string, error) {
if _, err := ffmpegCmd(); err != nil {
return "", err
}
args := createProbeCommand(probeCmd, files)
log.Trace(ctx, "Executing ffmpeg command", "args", args)
cmd := exec.CommandContext(ctx, args[0], args[1:]...) // #nosec
output, _ := cmd.CombinedOutput()
return string(output), nil
}
func (e *ffmpeg) CmdPath() (string, error) {
return ffmpegCmd()
}
func (e *ffmpeg) start(ctx context.Context, args []string) (io.ReadCloser, error) {
log.Trace(ctx, "Executing ffmpeg command", "cmd", args)
j := &Cmd{ctx: ctx, args: args}
j := &ffCmd{args: args}
j.PipeReader, j.out = io.Pipe()
err := j.start()
if err != nil {
@@ -49,16 +76,15 @@ func (e *ffmpeg) start(ctx context.Context, args []string) (io.ReadCloser, error
return j, nil
}
type Cmd struct {
type ffCmd struct {
*io.PipeReader
out *io.PipeWriter
ctx context.Context
args []string
cmd *exec.Cmd
}
func (j *Cmd) start() error {
cmd := exec.CommandContext(j.ctx, j.args[0], j.args[1:]...) // #nosec
func (j *ffCmd) start() error {
cmd := exec.Command(j.args[0], j.args[1:]...) // #nosec
cmd.Stdout = j.out
if log.CurrentLevel() >= log.LevelTrace {
cmd.Stderr = os.Stderr
@@ -73,7 +99,7 @@ func (j *Cmd) start() error {
return nil
}
func (j *Cmd) wait() {
func (j *ffCmd) wait() {
if err := j.cmd.Wait(); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
@@ -83,16 +109,12 @@ func (j *Cmd) wait() {
}
return
}
if j.ctx.Err() != nil {
_ = j.out.CloseWithError(j.ctx.Err())
return
}
_ = j.out.Close()
}
// Path will always be an absolute path
func createFFmpegCommand(cmd, path string, maxBitRate int) []string {
split := strings.Split(cmd, " ")
split := strings.Split(fixCmd(cmd), " ")
for i, s := range split {
s = strings.ReplaceAll(s, "%s", path)
s = strings.ReplaceAll(s, "%b", strconv.Itoa(maxBitRate))
@@ -101,3 +123,59 @@ func createFFmpegCommand(cmd, path string, maxBitRate int) []string {
return split
}
func createProbeCommand(cmd string, inputs []string) []string {
split := strings.Split(fixCmd(cmd), " ")
var args []string
for _, s := range split {
if s == "%s" {
for _, inp := range inputs {
args = append(args, "-i", inp)
}
} else {
args = append(args, s)
}
}
return args
}
func fixCmd(cmd string) string {
split := strings.Split(cmd, " ")
var result []string
cmdPath, _ := ffmpegCmd()
for _, s := range split {
if s == "ffmpeg" || s == "ffmpeg.exe" {
result = append(result, cmdPath)
} else {
result = append(result, s)
}
}
return strings.Join(result, " ")
}
func ffmpegCmd() (string, error) {
ffOnce.Do(func() {
if conf.Server.FFmpegPath != "" {
ffmpegPath = conf.Server.FFmpegPath
ffmpegPath, ffmpegErr = exec.LookPath(ffmpegPath)
} else {
ffmpegPath, ffmpegErr = exec.LookPath("ffmpeg")
if errors.Is(ffmpegErr, exec.ErrDot) {
log.Trace("ffmpeg found in current folder '.'")
ffmpegPath, ffmpegErr = exec.LookPath("./ffmpeg")
}
}
if ffmpegErr == nil {
log.Info("Found ffmpeg", "path", ffmpegPath)
return
}
})
return ffmpegPath, ffmpegErr
}
var (
ffOnce sync.Once
ffmpegPath string
ffmpegErr error
)

View File

@@ -16,9 +16,23 @@ func TestFFmpeg(t *testing.T) {
RunSpecs(t, "FFmpeg Suite")
}
var _ = Describe("createFFmpegCommand", func() {
It("creates a valid command line", func() {
args := createFFmpegCommand("ffmpeg -i %s -b:a %bk mp3 -", "/music library/file.mp3", 123)
Expect(args).To(Equal([]string{"ffmpeg", "-i", "/music library/file.mp3", "-b:a", "123k", "mp3", "-"}))
var _ = Describe("ffmpeg", func() {
BeforeEach(func() {
_, _ = ffmpegCmd()
ffmpegPath = "ffmpeg"
ffmpegErr = nil
})
Describe("createFFmpegCommand", func() {
It("creates a valid command line", func() {
args := createFFmpegCommand("ffmpeg -i %s -b:a %bk mp3 -", "/music library/file.mp3", 123)
Expect(args).To(Equal([]string{"ffmpeg", "-i", "/music library/file.mp3", "-b:a", "123k", "mp3", "-"}))
})
})
Describe("createProbeCommand", func() {
It("creates a valid command line", func() {
args := createProbeCommand(probeCmd, []string{"/music library/one.mp3", "/music library/two.mp3"})
Expect(args).To(Equal([]string{"ffmpeg", "-i", "/music library/one.mp3", "-i", "/music library/two.mp3", "-f", "ffmetadata"}))
})
})
})

View File

@@ -7,7 +7,6 @@ import (
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/pl"
)
func newBufferedScrobbler(ds model.DataStore, s Scrobbler, service string) *bufferedScrobbler {
@@ -57,7 +56,12 @@ func (b *bufferedScrobbler) run(ctx context.Context) {
b.sendWakeSignal()
})
}
<-pl.ReadOrDone(ctx, b.wakeSignal)
select {
case <-b.wakeSignal:
continue
case <-ctx.Done():
return
}
}
}

View File

@@ -16,10 +16,10 @@ import (
"github.com/navidrome/navidrome/utils/singleton"
)
const nowPlayingExpire = 60 * time.Minute
const maxNowPlayingExpire = 60 * time.Minute
type NowPlayingInfo struct {
TrackID string
MediaFile model.MediaFile
Start time.Time
Username string
PlayerId string
@@ -46,50 +46,58 @@ type playTracker struct {
func GetPlayTracker(ds model.DataStore, broker events.Broker) PlayTracker {
return singleton.GetInstance(func() *playTracker {
m := ttlcache.NewCache()
m.SkipTTLExtensionOnHit(true)
_ = m.SetTTL(nowPlayingExpire)
p := &playTracker{ds: ds, playMap: m, broker: broker}
p.scrobblers = make(map[string]Scrobbler)
for name, constructor := range constructors {
s := constructor(ds)
if conf.Server.DevEnableBufferedScrobble {
s = newBufferedScrobbler(ds, s, name)
}
p.scrobblers[name] = s
}
return p
return newPlayTracker(ds, broker)
})
}
// This constructor only exists for testing. For normal usage, the PlayTracker has to be a singleton, returned by
// the GetPlayTracker function above
func newPlayTracker(ds model.DataStore, broker events.Broker) *playTracker {
m := ttlcache.NewCache()
m.SkipTTLExtensionOnHit(true)
_ = m.SetTTL(maxNowPlayingExpire)
p := &playTracker{ds: ds, playMap: m, broker: broker}
p.scrobblers = make(map[string]Scrobbler)
for name, constructor := range constructors {
s := constructor(ds)
if conf.Server.DevEnableBufferedScrobble {
s = newBufferedScrobbler(ds, s, name)
}
p.scrobblers[name] = s
}
return p
}
func (p *playTracker) NowPlaying(ctx context.Context, playerId string, playerName string, trackId string) error {
mf, err := p.ds.MediaFile(ctx).Get(trackId)
if err != nil {
log.Error(ctx, "Error retrieving mediaFile", "id", trackId, err)
return err
}
user, _ := request.UserFrom(ctx)
info := NowPlayingInfo{
TrackID: trackId,
MediaFile: *mf,
Start: time.Now(),
Username: user.UserName,
PlayerId: playerId,
PlayerName: playerName,
}
_ = p.playMap.Set(playerId, info)
ttl := time.Duration(int(mf.Duration)+5) * time.Second
_ = p.playMap.SetWithTTL(playerId, info, ttl)
player, _ := request.PlayerFrom(ctx)
if player.ScrobbleEnabled {
p.dispatchNowPlaying(ctx, user.ID, trackId)
p.dispatchNowPlaying(ctx, user.ID, mf)
}
return nil
}
func (p *playTracker) dispatchNowPlaying(ctx context.Context, userId string, trackId string) {
t, err := p.ds.MediaFile(ctx).Get(trackId)
if err != nil {
log.Error(ctx, "Error retrieving mediaFile", "id", trackId, err)
return
}
func (p *playTracker) dispatchNowPlaying(ctx context.Context, userId string, t *model.MediaFile) {
if t.Artist == consts.UnknownArtist {
log.Debug(ctx, "Ignoring external NowPlaying update for track with unknown artist", "track", t.Title, "artist", t.Artist)
return
}
// TODO Parallelize
for name, s := range p.scrobblers {
if !s.IsAuthorized(ctx, userId) {
continue
@@ -103,7 +111,7 @@ func (p *playTracker) dispatchNowPlaying(ctx context.Context, userId string, tra
}
}
func (p *playTracker) GetNowPlaying(ctx context.Context) ([]NowPlayingInfo, error) {
func (p *playTracker) GetNowPlaying(_ context.Context) ([]NowPlayingInfo, error) {
var res []NowPlayingInfo
for _, playerId := range p.playMap.GetKeys() {
value, err := p.playMap.Get(playerId)

View File

@@ -37,7 +37,7 @@ var _ = Describe("PlayTracker", func() {
Register("fake", func(ds model.DataStore) Scrobbler {
return &fake
})
tracker = GetPlayTracker(ds, events.GetBroker())
tracker = newPlayTracker(ds, events.GetBroker())
track = model.MediaFile{
ID: "123",
@@ -93,16 +93,13 @@ var _ = Describe("PlayTracker", func() {
})
Describe("GetNowPlaying", func() {
BeforeEach(func() {
ctx = context.Background()
})
It("returns current playing music", func() {
track2 := track
track2.ID = "456"
_ = ds.MediaFile(ctx).Put(&track)
ctx = request.WithUser(ctx, model.User{UserName: "user-1"})
_ = ds.MediaFile(ctx).Put(&track2)
ctx = request.WithUser(context.Background(), model.User{UserName: "user-1"})
_ = tracker.NowPlaying(ctx, "player-1", "player-one", "123")
ctx = request.WithUser(ctx, model.User{UserName: "user-2"})
ctx = request.WithUser(context.Background(), model.User{UserName: "user-2"})
_ = tracker.NowPlaying(ctx, "player-2", "player-two", "456")
playing, err := tracker.GetNowPlaying(ctx)
@@ -112,12 +109,12 @@ var _ = Describe("PlayTracker", func() {
Expect(playing[0].PlayerId).To(Equal("player-2"))
Expect(playing[0].PlayerName).To(Equal("player-two"))
Expect(playing[0].Username).To(Equal("user-2"))
Expect(playing[0].TrackID).To(Equal("456"))
Expect(playing[0].MediaFile.ID).To(Equal("456"))
Expect(playing[1].PlayerId).To(Equal("player-1"))
Expect(playing[1].PlayerName).To(Equal("player-one"))
Expect(playing[1].Username).To(Equal("user-1"))
Expect(playing[1].TrackID).To(Equal("123"))
Expect(playing[1].MediaFile.ID).To(Equal("123"))
})
})

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

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

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

@@ -5,7 +5,7 @@ import (
"strings"
"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

@@ -5,7 +5,7 @@ import (
"github.com/google/uuid"
"github.com/navidrome/navidrome/consts"
"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() {

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

@@ -5,7 +5,7 @@ import (
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/utils"
"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

@@ -6,7 +6,7 @@ import (
"github.com/navidrome/navidrome/consts"
"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

@@ -8,7 +8,7 @@ import (
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/utils"
"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() {

View File

@@ -6,8 +6,7 @@ import (
"github.com/deluan/sanitize"
"github.com/navidrome/navidrome/log"
"github.com/pressly/goose"
"github.com/pressly/goose/v3"
)
func init() {

View File

@@ -5,7 +5,7 @@ import (
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/utils"
"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