Compare commits

...

466 Commits

Author SHA1 Message Date
Deluan
c556088820 Change dsf mime-type to audio/x-dsf.
Fix #3021
2024-05-12 11:33:50 -04:00
Deluan
78f554721a Revert "Add download link to PR" workflow 2024-05-11 20:40:12 -04:00
Deluan
2c8c87a980 Remove duplicated test 2024-05-11 20:15:02 -04:00
Deluan
3463d0c208 Simplify random.Int64 usage with generics 2024-05-11 20:10:46 -04:00
Deluan
0ae2944073 Refactor random functions 2024-05-11 20:04:21 -04:00
Deluan
30ae468dc1 Uses Unix milliseconds support from standard Go lib 2024-05-11 19:50:30 -04:00
Deluan
ec68d69d56 Refactor cache.HTTPClient 2024-05-11 19:37:12 -04:00
Deluan
955a9b43af Refactor merge.FS 2024-05-11 19:37:12 -04:00
Deluan
56809419c2 Fix "Add download link to PR" workflow 2024-05-11 18:50:46 -04:00
Deluan
3a2a5e961b Add samplingRate to OpenSubsonic responses 2024-05-11 17:57:45 -04:00
Deluan
f3bb022238 Add sampleRate to the DB 2024-05-11 17:57:45 -04:00
Deluan
472324e280 Read sampleRate from audio files 2024-05-11 17:57:45 -04:00
Deluan
ed83c22632 Do not panic if when updatePlaylist is called with a non-existent ID.
Fix #2876
2024-05-11 15:37:50 -04:00
edthu
2fdc1677f7 Add Catppuccin Macchiato Theme (#3014)
* Added Catppuccin Macchiato theme

* fixed index.js formatting
2024-05-11 13:08:51 -04:00
Deluan
80e68dfbcd Bump actions/github-script to v7 2024-05-10 16:00:21 -04:00
Deluan
a9c745839b Bump actions/stale and dessant/lock-threads versions 2024-05-10 15:51:16 -04:00
Deluan
bb96d455f8 Replace sync.WaitGroup with more appropriate errgroup.Group 2024-05-10 15:27:07 -04:00
Deluan
c0885b55db Fix M3U mimetype on Debian Bullseye 2024-05-09 22:26:15 -04:00
Deluan
00cbe4c357 Update Go to 1.22.3 2024-05-09 22:26:15 -04:00
Valeri Sokolov
2b49c7ff76 fix: languageName for Persian (#3011)
"انگلیسی" is "English"
2024-05-09 17:08:43 -04:00
Deluan
09d1fd0658 Simplify normalized AlbumPlayCountMode calc 2024-05-09 08:13:42 -04:00
Deluan
747069b229 Remove unused code 2024-05-09 07:47:32 -04:00
Deluan
885cd345ab Clean up runNavidrome function 2024-05-09 07:44:08 -04:00
Deluan Quintão
c4b05dac28 Make sorting lists by name/title case-insensitive (#2993)
* Make sort by order_* fields case-insensitive.

* Sort internet radios by name case-insensitive
2024-05-09 07:08:15 -04:00
Deluan Quintão
6408dda948 Terminate all MPV instances when stopping Navidrome (#3008)
* Terminate all mpv instances when stopping Navidrome

* Exit trackSwitcher goroutine when terminating

* Remove potential race condition when starting the Playback device

* Fix lint error

* Removed unused and unneeded vars/functions

* Use device short name in log

* Small refactor

* Small nitpick

* Make start functions more uniform
2024-05-09 06:57:24 -04:00
Deluan
677d9947f3 Make dependency injection more consistent 2024-05-08 22:21:38 -04:00
Deluan
a0290587b9 Fix migration package name mismatch 2024-05-08 19:54:48 -04:00
Deluan
eb93136b3f Change default transcodings to a proper typed struct 2024-05-08 17:39:25 -04:00
Deluan
62cc8a2d4b Fix ambiguous column when sorting media_files by created_at.
Fix #3006
2024-05-08 08:24:26 -04:00
Deluan
dd4374cec6 Limit access to Jukebox for admins only (configurable).
Closes #2849
2024-05-07 19:35:43 -04:00
Deluan
86567f5406 Bump Go dependencies 2024-05-07 19:26:02 -04:00
Matthias Schmidt
ff8dca5abe Guard against missing active track (#2996)
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2024-05-07 19:22:39 -04:00
Matthias Schmidt
b3d70e9264 Persist adjusted volume (#2997)
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2024-05-07 19:21:35 -04:00
Ludovic Fernandez
4d29184998 Improves golangci-lint configuration and workflow (#3004)
* chore: the default Go version is based on the go.mod

* chore: use linter configuration instead of exclude-rules

* chore: update workflow
2024-05-07 18:52:26 -04:00
Deluan
2470471b2b Pin golangci-lint-action version as a workaround to fix the pipeline.
See https://github.com/golangci/golangci-lint/issues/4695
2024-05-06 21:53:47 +02:00
Deluan
544ae90ec1 Fix CollapsibleComment in PlaylistDetails. Closes #2992 2024-05-02 13:48:10 -04:00
Deluan
aef49cb8d6 Add HTTPSecurityHeaders.CustomFrameOptionsValue option.
Requested in https://github.com/navidrome/navidrome/issues/248#issuecomment-1783768985
2024-05-02 12:35:16 -04:00
Deluan
7c5eec715d Fix typo 2024-05-01 23:09:11 -04:00
Kendall Garner
a4c2232041 Sort repeated lyrics that may be out of order (#2989)
With synchronized lyrics with repeated text, there is not a guarantee that the repeat is in order (e.g. `[00:00.00][00:10.00] a\n[00:05.00]b`).
This change will post-process lyrics with repeated timestamps in one line to ensure that it is always sorted.
2024-05-01 21:54:46 -04:00
Deluan
8f11b991d2 Bump Go dependencies 2024-05-01 20:40:34 -04:00
Deluan
d4a9a9e555 Fix PlaylistTracks's loadAllGenres. Fix #2988 2024-05-01 20:17:42 -04:00
Deluan
a8955f24e0 Fix AlbumPlayCountMode. Closes #2984 2024-05-01 20:05:36 -04:00
Deluan
2c06a4234e Fix int types in OpenSubsonic responses.
Refer to https://support.symfonium.app/t/symfonium-sync-crashes-when-tpos-is-not-an-int/4204
2024-05-01 13:57:11 -04:00
Deluan
7ab7b5df5e Fix signaler on Windows 2024-04-28 18:32:28 -04:00
Deluan
3d9fff36f7 Use signal.NotifyContext 2024-04-28 17:44:11 -04:00
Deluan
31fcab07d2 Refactor loadGenres, remove duplication 2024-04-28 17:04:12 -04:00
Deluan
de90152a71 Refactor DB Album mapping to model.Album 2024-04-28 13:51:57 -04:00
Deluan
27875ba2dd Load mime_types from external file 2024-04-28 12:18:24 -04:00
Deluan
28f7ef43c1 Remove AlbumPlayCountMode from command line options 2024-04-27 20:39:16 -04:00
Deluan
92a98cd558 Add tests for AlbumPlayCountMode, change the calc to match the request from #1032 2024-04-27 15:20:46 -04:00
Deluan
5d50558610 Add tests for AlbumPlayCountMode 2024-04-27 15:07:50 -04:00
vvdveen
8bff1ad512 Add AlbumPlayCountMode config option (#2803)
Closes #1032

* feat(album_repository.go): add kodi-style album playcount option - #1032

Signed-off-by: Victor van der Veen <vvdveen@gmail.com>

* fix format issue and remove reference to kodi (now normalized)

Signed-off-by: Victor van der Veen <vvdveen@gmail.com>

* reduced complexity but added rounding

Signed-off-by: Victor van der Veen <vvdveen@gmail.com>

* Use constants for AlbumPlayCountMode values

---------

Signed-off-by: Victor van der Veen <vvdveen@gmail.com>
Co-authored-by: Deluan <deluan@navidrome.org>
2024-04-27 14:10:40 -04:00
crazygolem
1e96b858a9 Add support for Reverse Proxy auth in Subsonic endpoints (#2558)
* feat(subsonic): Add support for Reverse Proxy auth - #2557

Signed-off-by: Jeremiah Menétrey <superjun1@gmail.com>

* Small refactoring

---------

Signed-off-by: Jeremiah Menétrey <superjun1@gmail.com>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2024-04-27 13:47:42 -04:00
Deluan
aafd5a952c Bump github.com/spf13/viper from 1.15.0 to 1.18.2 2024-04-26 22:11:43 -04:00
Deluan Quintão
d9cd5efd67 Bump Go dependencies (#2976)
* Fix build

* Bump dependencies
2024-04-26 18:21:10 -04:00
Deluan
affa9c3478 Bump github.com/pressly/goose/v3 from 3.19.2 to 3.20.0 2024-04-26 18:07:06 -04:00
Anna Smith
651a8fdaf9 Fix typo in comment (#2974) 2024-04-26 17:59:39 -04:00
Deluan
f7fc17c0f7 Add OpenSubsonic channelCount 2024-04-26 17:51:04 -04:00
Deluan
f5df948eb1 Fix scrobble error spam in the logs.
Relates to #2831 and #2975
2024-04-26 16:59:14 -04:00
crazygolem
18143fa5a1 Use the RealIP middleware also behind a reverse proxy (#2858)
* Use the RealIP middleware only behind a reverse proxy

* Fix proxy ip source in tests

* Fix test for PR#2087

The PR did not update the test after changing the behavior, but the test still
passed because another condition was preventing the user from being created in
the test.

* Use RealIP even without a trusted reverse proxy

* Use own type for context key

* Fix casing to follow go's conventions

* Do not apply RealIP middleware twice

* Fix IP source in logs

The most interesting data point in the log message is the proxy's IP, but
having the client IP too can help identify integration issues.
2024-04-25 20:43:58 -04:00
Tim
8f9ed1b994 Handling long playlist comments (#2973)
Closes #1737

* wrapping playlist comment in a <Collapse> element

* Extract common collapsible logic into a component

---------

Co-authored-by: Deluan <deluan@navidrome.org>
2024-04-25 20:28:25 -04:00
dependabot[bot]
cf66594b6d Bump github.com/onsi/gomega from 1.32.0 to 1.33.0 (#2968)
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.32.0 to 1.33.0.
- [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.32.0...v1.33.0)

---
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>
2024-04-22 17:09:51 -04:00
Deluan
ca005f6457 Include MPV in release Docker image. Refers to #2910 2024-04-21 21:02:36 -04:00
Deluan
6dcfe4d455 Fix typo 2024-04-20 13:16:50 -04:00
Deluan
7871d69adb Allow comments in the NSP file.
See comment https://github.com/navidrome/navidrome/issues/1417#issuecomment-2064731407
2024-04-20 12:50:45 -04:00
Deluan
78182f40d6 Block regular users from changing their own playlists ownership 2024-04-20 12:08:07 -04:00
Deluan
9aeaaa6610 Fix issue in https://github.com/navidrome/navidrome/issues/2767#issuecomment-2065636352 2024-04-19 12:38:02 -04:00
dependabot[bot]
068c1e9a23 Bump golang.org/x/net from 0.21.0 to 0.23.0 (#2962)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.21.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.21.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-19 09:15:08 -04:00
Jonathan
bcec15dc13 Externalize MPV command template (#2948)
* externalise MPVTemplate

* Remove unnecessary comment

---------

Co-authored-by: Deluan <deluan@navidrome.org>
2024-04-15 21:31:54 -04:00
dependabot[bot]
cf6603e3ec Bump react-icons from 5.0.1 to 5.1.0 in /ui (#2957)
Bumps [react-icons](https://github.com/react-icons/react-icons) from 5.0.1 to 5.1.0.
- [Release notes](https://github.com/react-icons/react-icons/releases)
- [Commits](https://github.com/react-icons/react-icons/compare/v5.0.1...v5.1.0)

---
updated-dependencies:
- dependency-name: react-icons
  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>
2024-04-15 14:35:00 -04:00
dependabot[bot]
88d6757121 Bump github.com/pelletier/go-toml/v2 from 2.2.0 to 2.2.1 (#2956)
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.2.0 to 2.2.1.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.2.0...v2.2.1)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/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>
2024-04-15 14:34:33 -04:00
Andrew Katsikas
c2f932c21c Fix jukebox mode under Windows (#2774)
* bug(core/playback/mpv): jukebox mode under windows - #2767

Use named pipe for socket path under windows during mpv playback, change function name, unexport function

Signed-off-by: apkatsikas <apkatsikas@gmail.com>

* bug(core/playback/mpv): jukebox mode under windows - #2767

Fix typo

Signed-off-by: apkatsikas <apkatsikas@gmail.com>

* bug(core/playback/mpv): jukebox mode under windows - navidrome#2767

Early return for Close on Windows

Signed-off-by: apkatsikas <apkatsikas@gmail.com>

* bug(core/playback/mpv): jukebox mode under windows - navidrome#2767

Update import and run prettier

Signed-off-by: apkatsikas <apkatsikas@gmail.com>

* bug(core/playback/mpv): jukebox mode under windows - navidrome#2767

Update function name

Signed-off-by: apkatsikas <apkatsikas@gmail.com>

* bug(core/playback/mpv): jukebox mode under windows - navidrome#2767

Create track_close files for both platforms and move MpvTrack Close into new file

Signed-off-by: apkatsikas <apkatsikas@gmail.com>

* bug(core/playback/mpv): jukebox mode under windows - navidrome#2767

Create SocketName function for both platforms, restore name of TempFileName

Signed-off-by: apkatsikas <apkatsikas@gmail.com>

* bug(core/playback/mpv): jukebox mode under windows - navidrome#2767

Add missing params to SocketName on windows

Signed-off-by: apkatsikas <apkatsikas@gmail.com>

* Unexport SocketName, use socketName in NewTrack

---------

Signed-off-by: apkatsikas <apkatsikas@gmail.com>
2024-04-14 13:50:37 -04:00
Deluan
d968f7f530 Remove deprecation warning about notify 2024-04-13 15:27:54 -04:00
dependabot[bot]
5fc78f120c Bump prettier from 3.2.2 to 3.2.5 in /ui (#2844)
Bumps [prettier](https://github.com/prettier/prettier) from 3.2.2 to 3.2.5.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.2.2...3.2.5)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  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>
2024-04-13 15:10:03 -04:00
dependabot[bot]
52dfa97262 Bump @testing-library/jest-dom from 6.2.0 to 6.4.2 in /ui (#2845)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 6.2.0 to 6.4.2.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v6.2.0...v6.4.2)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  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>
2024-04-13 15:09:53 -04:00
dependabot[bot]
c1eef058a4 Bump follow-redirects from 1.15.4 to 1.15.6 in /ui (#2911)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.4 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-13 15:09:34 -04:00
Deluan
7f551a7932 Add make target to build docker image 2024-04-13 13:29:45 -04:00
oftenoccur
bcb71b85c0 Fix some typos in comments (#2949)
Signed-off-by: oftenoccur <ezc5@sina.com>
2024-04-11 14:58:14 -04:00
Deluan
8720bd154f Ignore formatting diffs when checking for POEditor changes 2024-04-11 14:55:53 -04:00
Cyrille
699be19bb9 Fix a few mistakes in the French translation (#2872)
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2024-04-10 19:37:08 -04:00
looklose
22cc9e0cd5 Fix function name in comment (#2947)
Signed-off-by: looklose <shishuaiqun@yeah.net>
2024-04-10 12:53:21 -04:00
dependabot[bot]
6e36abdd62 Bump github.com/go-chi/jwtauth/v5 from 5.3.0 to 5.3.1
Bumps [github.com/go-chi/jwtauth/v5](https://github.com/go-chi/jwtauth) from 5.3.0 to 5.3.1.
- [Release notes](https://github.com/go-chi/jwtauth/releases)
- [Commits](https://github.com/go-chi/jwtauth/compare/v5.3.0...v5.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-09 20:45:43 -04:00
dependabot[bot]
e98c7374a9 Bump github.com/pelletier/go-toml/v2 from 2.1.1 to 2.2.0
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.1.1 to 2.2.0.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.1.1...v2.2.0)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-09 20:45:27 -04:00
Deluan Quintão
de7f553526 Update Go to 1.22.2 and TagLib to 2.0.1 (#2946) 2024-04-09 19:00:38 -04:00
dependabot[bot]
9cc0cc2e93 Bump github.com/pressly/goose/v3 from 3.18.0 to 3.19.2
Bumps [github.com/pressly/goose/v3](https://github.com/pressly/goose) from 3.18.0 to 3.19.2.
- [Release notes](https://github.com/pressly/goose/releases)
- [Changelog](https://github.com/pressly/goose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pressly/goose/compare/v3.18.0...v3.19.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>
2024-04-08 19:52:34 -04:00
dependabot[bot]
24298605d4 Bump github.com/onsi/ginkgo/v2 from 2.15.0 to 2.17.1
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.15.0 to 2.17.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.15.0...v2.17.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-08 19:46:24 -04:00
Deluan
4865d04ec6 Fix DiscTitle OpenSubsonic compatibility. Closes #2929 2024-04-08 19:05:36 -04:00
dependabot[bot]
81770351de Bump github.com/onsi/gomega from 1.31.1 to 1.32.0
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.31.1 to 1.32.0.
- [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.31.1...v1.32.0)

---
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>
2024-04-08 19:03:15 -04:00
dependabot[bot]
b6bbba754a Bump golang.org/x/sync from 0.6.0 to 0.7.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.6.0 to 0.7.0.
- [Commits](https://github.com/golang/sync/compare/v0.6.0...v0.7.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>
2024-04-08 18:57:52 -04:00
deluan
4f6121fae1 Update translations 2024-04-03 07:31:54 -04:00
Kendall Garner
f12dfb485a Expose OpenSubsonic release date for album (#2906)
* [enhancement]: OS expose release date for album, make original optional

* not optional

* remove omitempty
2024-04-03 07:30:01 -04:00
Deluan
e81bf5125f Bump actions versions 2024-04-02 19:37:59 -04:00
dependabot[bot]
a47acb6674 Bump github.com/lestrrat-go/jwx/v2 from 2.0.20 to 2.0.21
Bumps [github.com/lestrrat-go/jwx/v2](https://github.com/lestrrat-go/jwx) from 2.0.20 to 2.0.21.
- [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.20...v2.0.21)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-26 11:54:28 -04:00
dependabot[bot]
4a15677474 Bump google.golang.org/protobuf from 1.32.0 to 1.33.0
Bumps google.golang.org/protobuf from 1.32.0 to 1.33.0.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-26 11:53:51 -04:00
Deluan
859cdda0bd Bump Go dependencies 2024-03-03 21:30:28 -05:00
Deluan
87ecd118bb Bump goose to 3.18.0.
To fix the ambiguous import issue, I used:
go get -u google.golang.org/genproto/googleapis/rpc
2024-03-03 21:27:33 -05:00
Deluan
5abe156777 Logs don't panic when receiving a nil *time.Time 2024-02-18 13:06:01 -05:00
Deluan
fa72aaa462 Move TempFileName to utils 2024-02-18 12:52:06 -05:00
Deluan
6eb13c9f79 Run Test job in ci-goreleaser container 2024-02-18 12:52:06 -05:00
Deluan
b67d1c0830 Show taglib and ffmpeg versions in the log 2024-02-18 12:52:06 -05:00
Deluan
effd588406 Stop using deprecated TagLib method length 2024-02-18 12:52:06 -05:00
Deluan
6f4c55dbde Use new ci-goreleaser (with TagLib 2) 2024-02-18 12:52:06 -05:00
Deluan
176329343a Send Subsonic formatted response on marshalling errors 2024-02-17 10:39:29 -05:00
Deluan
97c7e5daaf Use new slices package from Go standard lib 2024-02-16 22:00:44 -05:00
Deluan
166eb37787 Use Go builtin min/max func 2024-02-16 21:53:16 -05:00
Deluan
f7a4387d0e Bump github.com/jellydator/ttlcache/v2 to v2.11.1 2024-02-16 21:42:22 -05:00
Deluan
71e5b271fb Bump github.com/xrash/smetrics version 2024-02-16 20:52:23 -05:00
Deluan
d51148ea4c Bump github.com/go-chi/chi/v5 to v5.0.12 2024-02-16 20:51:30 -05:00
Deluan
7cb8cc115e Bump github.com/mattn/go-sqlite3 to v1.14.22 2024-02-16 20:50:45 -05:00
Deluan
69d91189c2 Upgrade ginkgo and gomega 2024-02-16 20:49:37 -05:00
Deluan
88063fc189 Upgrade ginkgo and gomega 2024-02-16 20:47:53 -05:00
Deluan
912e144b71 Bump github.com/google/uuid to 1.6.0 2024-02-16 20:46:41 -05:00
Deluan
87484fe7a9 Bump github.com/google/wire to 0.6.0 2024-02-16 20:45:11 -05:00
Deluan
58f64355c2 Bump golang.org/x/exp version 2024-02-16 20:43:12 -05:00
Deluan Quintão
7167e5ac87 Upgrade to Go 1.22 and Node v20 (#2861)
* Remove workaround for missing `context.WithoutCancel` in Go 1.20

* Upgrade to Go 1.22

* Upgrade GitHub Actions

* Upgrade Node to v20
2024-02-16 20:29:16 -05:00
Deluan
d8e1748928 Return 500 in case of Subsonic response marshalling errors 2024-02-16 19:59:24 -05:00
Deluan
9a051967f6 Handle "Infinity" values for ReplayGain. Fix #2862 2024-02-16 18:44:58 -05:00
Deluan
0b2cf30096 Don't swallow marshalling errors in the Subsonic API 2024-02-16 18:43:36 -05:00
Deluan
6d253225de Use order/sort album/artist when sorting tracks in playlists. Fixes #2819 2024-02-15 21:52:00 -05:00
Caio Cotts
bf2bcb1279 Fix null values in DB (#2840)
* Fix album image_files being null.

* Fix small nitpick.

* Use ExecContext instead of Exec.

* Change more columns to not null and set default values.

* Remove columns that don't need to be changed from migration.

* Fix typo.

* Remove unnecessary select statements.

* Remove duplicate code.

* Do not apply changes to radio table.

* Do not apply changes full_text columns and respective indexes.

* Fix musicbrainz columns.

* Rename migration.

* Make ExternalInfoUpdatedAt nullable

* Make Share's timestamps nullable

---------

Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2024-02-07 20:45:08 -05:00
Deluan Quintão
ac4ceab143 Update French translation (#2834)
Co-authored-by: deluan <deluan@users.noreply.github.com>
2024-02-05 20:10:21 -05:00
Deluan
6226741517 Create resources.FS only once 2024-02-03 12:05:19 -05:00
Deluan
79a4d8f6ad Simplify ShortDur code and tests 2024-02-02 21:07:27 -05:00
Deluan Quintão
61257f89d2 Update translations (#2832)
Co-authored-by: deluan <deluan@users.noreply.github.com>
2024-01-30 07:25:42 -05:00
Deluan
1f71e56741 Don't expose Last.fm API Key in the index.html 2024-01-29 21:42:27 -05:00
Kendall Garner
3a9b3452a2 Set rating value to 0 when value is null (#2824) 2024-01-29 06:26:15 -05:00
Deluan
5125558f52 Make Subsonic search query default to "" if not present.
See https://github.com/orgs/music-assistant/discussions/414#discussioncomment-8265985
2024-01-27 20:00:02 -05:00
Deluan
5f9b6b632d Add a "upgrading schema" log message to the DB initialization when there are pending migrations. 2024-01-27 19:44:49 -05:00
Deluan
fa7cc40d23 Add tests for toSQL 2024-01-27 12:16:38 -05:00
caiocotts
58218e6dc4 Fix fields not being sent on getPlaylist.view responses. 2024-01-26 12:41:55 -05:00
Deluan
67c82f524b "Fix" Reddit badge 2024-01-24 20:24:13 -05:00
Deluan
fb7fd21984 Don't add empty TIPL roles 2024-01-24 19:22:25 -05:00
Deluan
a6fc84a2e1 Parse the ID3v2.4 TIPL frame 2024-01-23 20:50:43 -05:00
Deluan
1e5e8be192 Import ID3 sort_* tags 2024-01-23 18:07:11 -05:00
Deluan
fd61b29a84 Small readability improvement in MergeFS tests 2024-01-21 16:20:47 -05:00
Deluan
2b33ef72e3 Remove offset and limit from count queries. Fixes #2443 2024-01-20 22:02:05 -05:00
Deluan
2fb913f5c9 Add log message to try to capture error in #2735 2024-01-20 20:18:59 -05:00
Deluan
6c05493cda Improve some Jukebox error messages 2024-01-20 20:10:32 -05:00
Deluan
3ca4f44118 Simplify default middlewares setup 2024-01-20 19:17:21 -05:00
Deluan
34c29a156f Simplify RealIP middleware setup 2024-01-20 18:58:12 -05:00
dependabot[bot]
b442736a0f Bump connected-react-router from 6.9.1 to 6.9.3 in /ui (#2741)
Bumps [connected-react-router](https://github.com/supasate/connected-react-router) from 6.9.1 to 6.9.3.
- [Release notes](https://github.com/supasate/connected-react-router/releases)
- [Commits](https://github.com/supasate/connected-react-router/compare/v6.9.1...v6.9.3)

---
updated-dependencies:
- dependency-name: connected-react-router
  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>
2024-01-20 18:42:43 -05:00
dependabot[bot]
90fccf00d1 Bump workbox-cli from 6.5.4 to 7.0.0 in /ui (#2737)
Bumps [workbox-cli](https://github.com/googlechrome/workbox) from 6.5.4 to 7.0.0.
- [Release notes](https://github.com/googlechrome/workbox/releases)
- [Commits](https://github.com/googlechrome/workbox/compare/v6.5.4...v7.0.0)

---
updated-dependencies:
- dependency-name: workbox-cli
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-20 18:38:44 -05:00
dependabot[bot]
bcd4a52616 Bump golang.org/x/sync from 0.5.0 to 0.6.0 (#2779)
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.5.0 to 0.6.0.
- [Commits](https://github.com/golang/sync/compare/v0.5.0...v0.6.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>
2024-01-20 18:37:46 -05:00
dependabot[bot]
84cffa6b94 Bump github.com/prometheus/client_golang from 1.17.0 to 1.18.0 (#2759)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.17.0 to 1.18.0.
- [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.17.0...v1.18.0)

---
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>
2024-01-20 18:37:01 -05:00
dependabot[bot]
a51b1b25d2 Bump uuid from 8.3.2 to 9.0.1 in /ui (#2740)
Bumps [uuid](https://github.com/uuidjs/uuid) from 8.3.2 to 9.0.1.
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v8.3.2...v9.0.1)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-20 18:36:14 -05:00
dependabot[bot]
9f317c054b Bump @testing-library/user-event from 14.5.1 to 14.5.2 in /ui (#2757)
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 14.5.1 to 14.5.2.
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v14.5.1...v14.5.2)

---
updated-dependencies:
- dependency-name: "@testing-library/user-event"
  dependency-type: direct:development
  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>
2024-01-20 18:35:20 -05:00
dependabot[bot]
5f8d01a207 Bump clsx from 2.0.0 to 2.1.0 in /ui (#2758)
Bumps [clsx](https://github.com/lukeed/clsx) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/lukeed/clsx/releases)
- [Commits](https://github.com/lukeed/clsx/compare/v2.0.0...v2.1.0)

---
updated-dependencies:
- dependency-name: clsx
  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>
2024-01-20 18:35:08 -05:00
dependabot[bot]
8a648d717a Bump github.com/go-chi/chi/v5 from 5.0.10 to 5.0.11 (#2742)
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.10 to 5.0.11.
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.0.10...v5.0.11)

---
updated-dependencies:
- dependency-name: github.com/go-chi/chi/v5
  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>
2024-01-20 18:34:58 -05:00
dependabot[bot]
a0dc2ee051 Bump github.com/pelletier/go-toml/v2 from 2.0.6 to 2.1.1 (#2760)
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.0.6 to 2.1.1.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.0.6...v2.1.1)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  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>
2024-01-20 18:32:53 -05:00
dependabot[bot]
ffb4de1e27 Bump github.com/unrolled/secure from 1.13.0 to 1.14.0 (#2761)
Bumps [github.com/unrolled/secure](https://github.com/unrolled/secure) from 1.13.0 to 1.14.0.
- [Release notes](https://github.com/unrolled/secure/releases)
- [Commits](https://github.com/unrolled/secure/compare/v1.13.0...v1.14.0)

---
updated-dependencies:
- dependency-name: github.com/unrolled/secure
  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>
2024-01-20 18:31:50 -05:00
dependabot[bot]
e1fc7983a5 Bump golang.org/x/image from 0.14.0 to 0.15.0 (#2778)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.14.0 to 0.15.0.
- [Commits](https://github.com/golang/image/compare/v0.14.0...v0.15.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>
2024-01-20 18:31:18 -05:00
dependabot[bot]
2a43f54eb1 Bump follow-redirects from 1.15.2 to 1.15.4 in /ui (#2786)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-20 18:30:55 -05:00
dependabot[bot]
f654e92113 Bump github.com/lestrrat-go/jwx/v2 from 2.0.18 to 2.0.19 (#2792)
Bumps [github.com/lestrrat-go/jwx/v2](https://github.com/lestrrat-go/jwx) from 2.0.18 to 2.0.19.
- [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.18...v2.0.19)

---
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>
2024-01-20 18:29:46 -05:00
flyingOwl
dfa453cc4a Add (not)inplaylist operator to smart playlists (#1884)
Closes #1417 

A smart playlist can use the playlist id for filtering. This can be
used to create combined playlists or to filter multiple playlists.

To filter by a playlist id, a subquery is created that will match the
media ids with the playlists within the playlist_tracks table.

Signed-off-by: flyingOwl <ofenfisch@googlemail.com>
2024-01-20 18:22:17 -05:00
Johannes Engl
8f03454312 Make server unix socket file permission configurable via flag UnixSocketPerm (#2763)
* feat(any): Add flag unixsocketperm with default 0017 - #2625

Signed-off-by: johannesengl <hello@johannesengl.com>

* feat(server): Update unix socket file perm based on config - #2625

Signed-off-by: johannesengl <hello@johannesengl.com>

* Fix default value of socket.

* Refactor unix socket file creation.

* Remove misplaced comment

---------

Signed-off-by: johannesengl <hello@johannesengl.com>
Co-authored-by: Caio Cotts <caio@cotts.com.br>
Co-authored-by: Deluan <deluan@navidrome.org>
2024-01-20 14:50:30 -05:00
dependabot[bot]
8570773b90 Bump prettier from 3.1.1 to 3.2.2 in /ui
Bumps [prettier](https://github.com/prettier/prettier) from 3.1.1 to 3.2.2.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.1.1...3.2.2)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-18 09:23:10 -05:00
caiocotts
6cff91e17d Use the default import path for jest-dom. 2024-01-17 17:07:43 -05:00
dependabot[bot]
d0df81a8df Bump @testing-library/jest-dom from 5.16.5 to 6.2.0 in /ui
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.16.5 to 6.2.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.16.5...v6.2.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-17 17:07:43 -05:00
dependabot[bot]
75f3ef64e2 Bump react-icons from 4.4.0 to 5.0.1 in /ui
Bumps [react-icons](https://github.com/react-icons/react-icons) from 4.4.0 to 5.0.1.
- [Release notes](https://github.com/react-icons/react-icons/releases)
- [Commits](https://github.com/react-icons/react-icons/compare/v4.4.0...v5.0.1)

---
updated-dependencies:
- dependency-name: react-icons
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-17 16:10:00 -05:00
dependabot[bot]
170ac93926 Bump github.com/onsi/ginkgo/v2 from 2.13.2 to 2.14.0
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.13.2 to 2.14.0.
- [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.13.2...v2.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-17 15:58:57 -05:00
Deluan
6f7b48202e Make the GetInstance concurrent test more readable 2023-12-28 16:50:07 -05:00
Deluan
6e2be7f95f Don't force a full scan after upgrading the lyrics 2023-12-28 04:55:45 -05:00
Deluan
0d8f8e3afd Optimize Singleton (sometimes a simple lock is a better solution) 2023-12-27 22:12:34 -05:00
Deluan
e50382e3bf Fix ReplayGain values not being retrieved from DB 2023-12-27 21:14:54 -05:00
Kendall Garner
814161d78d Add OS Lyrics extension (#2656)
* draft commit

* time to fight pipeline

* round 2 changes

* remove unnecessary line

* fight taglib. again

* make taglib work again???

* add id3 tags

* taglib 1.12 vs 1.13

* use int instead for windows

* store as json now

* add migration, more tests

* support repeated line, multiline

* fix ms and support .m, .mm, .mmm

* address some concerns, make cpp a bit safer

* separate responses from model

* remove [:]

* Add trace log

* Try to unblock pipeline

* Fix merge errors

* Fix SIGSEGV error (proper handling of empty frames)

* Add fallback artist/title to structured lyrics

* Rename conflicting named vars

* Fix tests

* Do we still need ffmpeg in the pipeline?

* Revert "Do we still need ffmpeg in the pipeline?"

Yes we do.

This reverts commit 87df7f6df7.

* Does this passes now, with a newer ffmpeg version?

* Revert "Does this passes now, with a newer ffmpeg version?"

No, it does not :(

This reverts commit 372eb4b0ae.

* My OCD made me do it :P

---------

Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2023-12-27 20:20:29 -05:00
Deluan
130ab76c79 go mod tidy 2023-12-27 13:04:26 -05:00
Deluan
a186a795f6 Omit empty Genre attributes 2023-12-27 12:44:25 -05:00
Deluan
798b03eabd Add "inspect" command to CLI 2023-12-27 12:41:28 -05:00
Deluan
ea7ba22699 Discard duplicated tags 2023-12-26 19:35:14 -05:00
Andrew Katsikas
b4815ecee5 Add TAK support (#2745)
* bug(consts/mime_types): tak-support - 2514

Add tak to mime_types audioFormats

Signed-off-by: apkatsikas <apkatsikas@gmail.com>

* bug(scanner): tak-support - 2514

Add tak test fixture file and add fixes for tag_scanner and walk_dir_tree tests

Signed-off-by: apkatsikas <apkatsikas@gmail.com>

* Remove comment

---------

Signed-off-by: apkatsikas <apkatsikas@gmail.com>
2023-12-26 18:39:15 -05:00
Deluan
51e07d4cb5 Add log.IsGreaterOrEqualTo, that take into consideration path-scoped log levels 2023-12-25 16:35:16 -05:00
Deluan
03119e5ccf Add more trace log to TagLib Wrapper 2023-12-23 14:10:38 -05:00
Deluan Quintão
15e1394fa3 Implement originalReleaseDate in OpenSubsonic responses. (#2733)
See https://github.com/opensubsonic/open-subsonic-api/pull/80
2023-12-22 21:03:55 -05:00
Deluan
3f349b1b58 Add todo as a reminder to replace min/max in Go 1.22 2023-12-21 19:19:46 -05:00
Deluan
dfcc189cff Replace all utils.Param* with req.Params 2023-12-21 17:41:09 -05:00
Deluan
00597e01e9 Add req.Params to replace utils.Param* 2023-12-21 16:32:37 -05:00
Dany Marcoux
965fc9d9be Remove beep and the files where it was imported (#2731)
Beep isn't needed anymore since we rely on MPV instead.

The changes to `go.mod` and `go.sum` were done with:
```
go get github.com/faiface/beep@none
go mod tidy
```

Signed-off-by: Dany Marcoux <git@dmarcoux.com>
2023-12-21 08:00:31 -05:00
Deluan Quintão
781ff40464 Bump Go version to 1.21.5 (#2729) 2023-12-20 20:02:40 -05:00
Deluan
a6ed0442f2 Name mapDates return values 2023-12-20 16:29:39 -05:00
dependabot[bot]
515efe37f0 Bump @testing-library/user-event from 13.5.0 to 14.5.1 in /ui
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 13.5.0 to 14.5.1.
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v13.5.0...v14.5.1)

---
updated-dependencies:
- dependency-name: "@testing-library/user-event"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-19 13:18:13 -05:00
dependabot[bot]
6c28c111bb Bump @adobe/css-tools from 4.3.1 to 4.3.2 in /ui
Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.3.1 to 4.3.2.
- [Changelog](https://github.com/adobe/css-tools/blob/main/History.md)
- [Commits](https://github.com/adobe/css-tools/commits)

---
updated-dependencies:
- dependency-name: "@adobe/css-tools"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-19 12:34:13 -05:00
dependabot[bot]
92a88ad4d9 Bump golang.org/x/crypto from 0.16.0 to 0.17.0 (#2722)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.16.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.16.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-18 17:45:37 -05:00
dependabot[bot]
4ccc0a92bf Bump jwt-decode from 3.1.2 to 4.0.0 in /ui (#2714)
* Bump jwt-decode from 3.1.2 to 4.0.0 in /ui

Bumps [jwt-decode](https://github.com/auth0/jwt-decode) from 3.1.2 to 4.0.0.
- [Release notes](https://github.com/auth0/jwt-decode/releases)
- [Changelog](https://github.com/auth0/jwt-decode/blob/main/CHANGELOG.md)
- [Commits](https://github.com/auth0/jwt-decode/compare/v3.1.2...v4.0.0)

---
updated-dependencies:
- dependency-name: jwt-decode
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Make jwt-decode a named import.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Caio Cotts <caio@cotts.com.br>
2023-12-18 17:28:42 -05:00
dependabot[bot]
df3de047ca Bump clsx from 1.1.1 to 2.0.0 in /ui
Bumps [clsx](https://github.com/lukeed/clsx) from 1.1.1 to 2.0.0.
- [Release notes](https://github.com/lukeed/clsx/releases)
- [Commits](https://github.com/lukeed/clsx/compare/v1.1.1...v2.0.0)

---
updated-dependencies:
- dependency-name: clsx
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-18 15:15:00 -05:00
Caio Cotts
86757663d6 Reformat code with Prettier's new rules. 2023-12-18 15:12:24 -05:00
dependabot[bot]
735d670a5b Bump prettier from 2.8.2 to 3.1.1 in /ui
Bumps [prettier](https://github.com/prettier/prettier) from 2.8.2 to 3.1.1.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.8.2...3.1.1)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-18 15:12:24 -05:00
dependabot[bot]
30179146c3 Bump deepmerge from 4.2.2 to 4.3.1 in /ui
Bumps [deepmerge](https://github.com/TehShrike/deepmerge) from 4.2.2 to 4.3.1.
- [Changelog](https://github.com/TehShrike/deepmerge/blob/master/changelog.md)
- [Commits](https://github.com/TehShrike/deepmerge/compare/v4.2.2...v4.3.1)

---
updated-dependencies:
- dependency-name: deepmerge
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-18 14:22:11 -05:00
dependabot[bot]
03a9f22ed9 Bump @material-ui/icons from 4.11.2 to 4.11.3 in /ui
Bumps [@material-ui/icons](https://github.com/mui-org/material-ui/tree/HEAD/packages/material-ui-icons) from 4.11.2 to 4.11.3.
- [Release notes](https://github.com/mui-org/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/v4.11.3/CHANGELOG.md)
- [Commits](https://github.com/mui-org/material-ui/commits/v4.11.3/packages/material-ui-icons)

---
updated-dependencies:
- dependency-name: "@material-ui/icons"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-18 14:17:25 -05:00
dependabot[bot]
39e92a1918 Bump github.com/mattn/go-sqlite3 from 1.14.18 to 1.14.19
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.18 to 1.14.19.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.18...v1.14.19)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-18 14:07:07 -05:00
Deluan
421ce91a9e Fix mpvipc dependency 2023-12-17 13:57:15 -05:00
Deluan
12aae5e951 Some cleanup in the jukebox code, specially log messages 2023-12-17 13:15:47 -05:00
Deluan
932152eb7e Change required fields in Subsonic Jukebox endpoint
See discussion here: https://gitlab.com/ultrasonic/ultrasonic/-/issues/1266#note_1621953651
2023-12-17 13:15:47 -05:00
Deluan
0e3175ea17 Better workaround for Go 1.20 missing context.WithoutCancel 2023-12-16 13:33:03 -05:00
Deluan
d3f6b4692d Temporary fix for scan context cancellation for Go 1.20 2023-12-15 07:59:34 -05:00
Deluan
70effa09e8 Don't cancel Scan on context cancellation 2023-12-14 22:52:48 -05:00
Deluan
7ccf685973 Fix PreferSortTags 2023-12-14 21:45:47 -05:00
Deluan
2aef227572 Add context to SQL queries, enabling cancellation 2023-12-14 17:13:09 -05:00
Deluan
d80e1a260b Fix possible authentication bypass 2023-12-13 19:32:05 -05:00
dependabot[bot]
fd4605d7dc Bump github.com/mattn/go-zglob from 0.0.3 to 0.0.4 (#2015)
Bumps [github.com/mattn/go-zglob](https://github.com/mattn/go-zglob) from 0.0.3 to 0.0.4.
- [Release notes](https://github.com/mattn/go-zglob/releases)
- [Commits](https://github.com/mattn/go-zglob/compare/v0.0.3...v0.0.4)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-zglob
  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-12-13 17:27:42 -05:00
Deluan
a6493c4c36 Bump github.com/google/uuid to v1.5.0 2023-12-13 16:47:05 -05:00
Kendall Garner
54597bd575 Allow reverse proxy auth for unix socket (#2701) 2023-12-12 06:06:27 -05:00
Deluan Quintão
ab53313273 Add new PrefSortTags option (#2696) 2023-12-11 20:37:11 -05:00
Deluan
d3669f46a9 go mod tidy 2023-12-11 19:03:27 -05:00
Deluan
d89de9060a Bump Go dependencies 2023-12-11 17:25:14 -05:00
Deluan
ac3668a33e Removed unused diodes package 2023-12-11 17:22:10 -05:00
dependabot[bot]
6d924ad742 Bump github.com/go-chi/jwtauth/v5 from 5.2.0 to 5.3.0 (#2699)
Bumps [github.com/go-chi/jwtauth/v5](https://github.com/go-chi/jwtauth) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/go-chi/jwtauth/releases)
- [Commits](https://github.com/go-chi/jwtauth/compare/v5.2.0...v5.3.0)

---
updated-dependencies:
- dependency-name: github.com/go-chi/jwtauth/v5
  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-12-11 13:41:03 -05:00
Deluan
78d557c185 Remove LastFM shared key 2023-12-10 21:11:40 -05:00
Deluan
546aa26a0a Removed duplicated code 2023-12-09 14:11:07 -05:00
dependabot[bot]
fc677f7951 Bump github.com/lestrrat-go/jwx/v2 from 2.0.17 to 2.0.18 (#2684)
Bumps [github.com/lestrrat-go/jwx/v2](https://github.com/lestrrat-go/jwx) from 2.0.17 to 2.0.18.
- [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.17...v2.0.18)

---
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-12-09 14:04:56 -05:00
Deluan
aed0309161 Return AlbumID3 in search3 results 2023-12-09 14:01:22 -05:00
Deluan
465cc091b0 Convert internal disc number representation to int 2023-12-09 13:53:38 -05:00
Deluan
2c9035fdd0 Add discTitles to OpenSubsonic responses 2023-12-09 13:53:38 -05:00
Deluan
af7eead037 Add discs to album 2023-12-09 13:53:38 -05:00
Deluan Quintão
0ca0d5da22 Replace beego/orm with dbx (#2693)
* Start migration to dbx package

* Fix annotations and bookmarks bindings

* Fix tests

* Fix more tests

* Remove remaining references to beego/orm

* Add PostScanner/PostMapper interfaces

* Fix importing SmartPlaylists

* Renaming

* More renaming

* Fix artist DB mapping

* Fix playlist updates

* Remove bookmarks at the end of the test

* Remove remaining `orm` struct tags

* Fix user timestamps DB access

* Fix smart playlist evaluated_at DB access

* Fix search3
2023-12-09 13:52:17 -05:00
dependabot[bot]
7074455e0e Bump github.com/onsi/ginkgo/v2 from 2.13.1 to 2.13.2
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.13.1 to 2.13.2.
- [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.13.1...v2.13.2)

---
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>
2023-12-08 21:35:42 -05:00
caiocotts
2f2fbeb009 Fix ld warnings on taglib_wrapper. 2023-12-04 15:19:12 -05:00
Kendall Garner
742fd16a01 Parse more itunes keys, optimize taglib wrapper (#2680)
* parse more itunes keys

* Move special iTunes M4A logic to Go code

* Simplify ASF/WMA tags handling

* Simplify ASF/WMA tags handling even more, moving compilation logic to `metadata` normalizer

* Remove strdups from C++ code, `C.GoString` already duplicates the strings

* reduced set

* remove strdup

* Small nitpick

---------

Co-authored-by: Deluan <deluan@navidrome.org>
2023-12-03 14:19:16 -05:00
Deluan Quintão
7766ee069c Return http form post extension (OpenSubsonic) (#2676) 2023-12-02 19:46:57 -05:00
Deluan
4cd7c7f39f Fix FileHaunter tests 2023-12-02 19:40:59 -05:00
Deluan
81daee3b9b Fix FileHaunter tests 2023-12-02 18:43:24 -05:00
Deluan
9b434d743f Ignore flaky FileHaunter tests 2023-12-02 18:32:48 -05:00
Deluan
4641dc0b2b Add ReplayGain to OpenSubsonic API Child response 2023-12-02 15:28:44 -05:00
Deluan
812dc2090f Add support for timeOffset in /stream endpoint 2023-12-02 13:10:36 -05:00
Deluan
a9cf54afef Return genres in bookmark endpoints (OpenSubsonic) 2023-12-02 11:36:16 -05:00
Deluan
595186b1b2 Coalesce null annotation values, to better rank them against annotations with value 0 2023-12-02 11:35:54 -05:00
Deluan
cdccdc56c9 Add more OpenSubsonic fields
- isCompilation
- sortName
2023-11-28 21:26:00 -05:00
Deluan
f580c5b8bc Add more OpenSubsonic fields
- mediaType
- musicBrainzId (Child)
2023-11-28 21:12:28 -05:00
deluan
f0e25c251d Update translations 2023-11-28 06:10:03 -05:00
Deluan
abde399e7b Upgrade to Goose 3.15.1 2023-11-27 14:46:44 -05:00
Deluan
1b4483d32b Remove tools.go 2023-11-27 14:06:00 -05:00
Deluan
f7fe8ba938 npx update-browserslist-db@latest 2023-11-27 13:56:16 -05:00
Deluan
f543e7accc Fix getOpenSubsonicExtensions endpoint
Match the current doc: https://opensubsonic.netlify.app/docs/endpoints/getopensubsonicextensions/

openSubsonicExtensions must be an array, not a struct
2023-11-27 13:27:10 -05:00
Deluan Quintão
60a5fbe1fe Optimize search3, by removing OFFSET when paginating (#2655)
* Optimize pagination, removing offset

* For search, don't add `where` clause for empty queries

* Revert "Replace `COUNT(DISTINCT primary_key)` with `COUNT(*)`"

Genres are required as part of the count queries, so filter by genres work

* Optimize search3 query, using order by id if it is a "" query.

Also fix the optimizePagination query logic

* Allow offset optimizer threshold to be configured
2023-11-27 13:06:23 -05:00
Deluan
28dc98dec4 Revert "Replace COUNT(DISTINCT primary_key) with COUNT(*)"
Genres are required as part of the count queries, so filter by genres work
2023-11-25 23:08:20 -05:00
Deluan
8c8e1ea701 Replace COUNT(DISTINCT primary_key) with COUNT(*) 2023-11-25 22:46:15 -05:00
Deluan
b964018cd7 Show SQL errors in queryAll 2023-11-25 13:54:38 -05:00
Deluan
9aa7b80d0d Generalize BreakUp/RangByChunks functions 2023-11-25 12:13:36 -05:00
Deluan
c3efc57259 Use TagLib 1.13.1 for snapshots/releases 2023-11-24 20:35:38 -05:00
Deluan
27a92b05e7 Fixed deprecated GoReleaser options 2023-11-24 18:08:34 -05:00
Deluan
21f1354cd1 Revert "Bump golang.org/x/exp, change slices.SortFunc function call"
This reverts commit 474f32f1
2023-11-24 17:57:22 -05:00
Deluan
069da5d91c Bump Go to 1.21.4 2023-11-24 17:51:36 -05:00
Deluan
69d2ced852 Bump Go dependencies 2023-11-24 16:45:52 -05:00
Deluan
17ac8d25cb Bump dependencies 2023-11-24 16:40:20 -05:00
Deluan
474f32f1b8 Bump golang.org/x/exp, change slices.SortFunc function call 2023-11-24 16:38:47 -05:00
Deluan
ecadcfb403 Make ParamInt generic (any int type) 2023-11-23 13:40:06 -05:00
Caio Cotts
f69c27d146 Return genres in search3 endpoint. 2023-11-21 21:34:03 -05:00
Caio Cotts
bb7186ce2f Fix marshaling for genres. 2023-11-21 21:34:03 -05:00
dependabot[bot]
5d1493e845 Bump @adobe/css-tools from 4.0.1 to 4.3.1 in /ui
Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.0.1 to 4.3.1.
- [Changelog](https://github.com/adobe/css-tools/blob/main/History.md)
- [Commits](https://github.com/adobe/css-tools/commits)

---
updated-dependencies:
- dependency-name: "@adobe/css-tools"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-21 08:47:14 -05:00
Deluan
d0fe406800 Fix Go 1.20 build 2023-11-21 08:32:22 -05:00
Deluan
c8fbf6b60e Bump dependencies 2023-11-21 08:22:02 -05:00
deluan
e5bc3ca200 Update translations 2023-11-21 08:15:32 -05:00
tarokeitaro
6d88dd2c66 Add Indonesian Language 2023-11-21 08:06:51 -05:00
caiocotts
eebfbc5381 Revert walk_dir_tree.go back to using the os package. 2023-11-21 07:17:22 -05:00
Deluan
a5dfd2d4a1 Format subsonic response snapshots 2023-11-18 14:43:40 -05:00
Drew Weymouth
7773522803 Expose OpenSubsonic fields Genres, MusicBrainzId, Bpm, Comment (#2597)
* add Genres to subsonic responses

* add genres in GetAlbum response

* add musicBrainzId

* add Bpm and Comment OpenSubsonic fields

* remove omitempty on OpenSubsonic fields

* add custom JSON marshalers to ensure genres attribute is non-nil

* regenerate snapshots to capture now-mandatory fields
2023-11-18 14:40:00 -05:00
Deluan
53607fe114 Publish all new images to Docker Registry 2023-11-16 23:21:20 -05:00
Caio Cotts
fee0f40a52 Bump dependencies 2023-11-16 20:38:45 -05:00
Caio Cotts
9d2aaff8cb Bump golang.org/x/tools from 0.13.0 to 0.15.0 2023-11-16 20:19:29 -05:00
Caio Cotts
2ff4023cce Bump golang.org/x/image from 0.12.0 to 0.14.0 2023-11-16 20:16:25 -05:00
Kendall Garner
79870b1090 Do not empty old artist metadata (#2423) 2023-11-16 19:20:37 -05:00
Kendall Garner
7a858a2db3 Fix external link for artist page if LastFM is missinb but Musicbrainz is not (#2533)
* fix mbz link if lastfm does not exist

* use lastfmUrl field

* fix artist info undefined
2023-11-16 19:07:52 -05:00
dependabot[bot]
9cefaf66a4 Bump github.com/onsi/gomega from 1.29.0 to 1.30.0
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.29.0 to 1.30.0.
- [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.29.0...v1.30.0)

---
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>
2023-11-16 18:45:21 -05:00
Kendall Garner
3debd31b12 Add more replaygain tests, fix wma (#2356)
* add more replaygain tests, fix wma

* Convert individual specs to a table spec

* Fix pipeline, by commenting incompatible tests

---------

Co-authored-by: Deluan <deluan@navidrome.org>
2023-11-14 20:25:18 -05:00
Deluan Quintão
24d9fb5b48 Update translations (#2409)
Co-authored-by: deluan <deluan@users.noreply.github.com>
2023-11-14 19:21:26 -05:00
certuna
40841ab917 Small date mapping fix (#2584)
* Update mapping.go

fallback in the case there's no Date tagged but Original Date or Release Date are tagged

* Add tests

---------

Co-authored-by: Deluan <deluan@navidrome.org>
2023-11-11 17:13:07 -05:00
certuna
bae5fc946b Fix hardcoded IPv4 literals (#2602)
* Update server_test.go

no hardcoded IPv4 literals

* Update package.json

no hardcoded IPv4 literals
2023-11-11 16:46:53 -05:00
Deluan
e055826068 Fix devcontainer for Go 1.21 2023-11-09 18:23:44 -05:00
dependabot[bot]
54bde266b4 Bump github.com/mattn/go-sqlite3 from 1.14.16 to 1.14.18
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.16 to 1.14.18.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.16...v1.14.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-07 18:24:08 -05:00
dependabot[bot]
3a7376901b Bump golang.org/x/sync from 0.3.0 to 0.5.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.3.0 to 0.5.0.
- [Commits](https://github.com/golang/sync/compare/v0.3.0...v0.5.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>
2023-11-07 18:19:32 -05:00
dependabot[bot]
de3d870100 Bump github.com/spf13/cobra from 1.7.0 to 1.8.0
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.7.0...v1.8.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>
2023-11-07 18:08:13 -05:00
certuna
03175e1a9d Use file creation date for Date Added/CreatedAt (#2553)
* Update mapping.go

CreatedAt = BirthTime

* Update metadata.go

Add BirthTime() function

* Update spread_fs.go

Replacing djherbis/atime package with djherbis/times, as times includes the functionality of atime

* Update go.mod

remove djherbis/atime, add djherbis/times

* Update mapping.go

time package not used anymore

* Update go.sum

removed djherbis/atime, added djherbis/times

* Update spread_fs.go

revert to previous, cannot get rid of /atime after all since it's a dependency of /fscache

* Update go.mod

djherbis/times 1.6.0 now released

* Update go.sum

new sums

* Update metadata.go

Inverted if statement, more readable

* Update go.mod

format fix

* Update go.sum

format fix

* Update go.sum

format fix

* Update go.sum

format fix

* Update metadata.go

variable name times -> fileProperties
check for errors

* Update metadata.go

reverse order of error check

* Update metadata.go

typo

* Update metadata.go

https://github.com/navidrome/navidrome/pull/2553#issuecomment-1787967615
2023-11-01 16:41:07 -04:00
Sam Watson
26472f46fe POST endpoint for importing m3u playlists - #2078 (#2273)
* wip: API endpoint for creating playlists from m3u files

* wip: get user id from context

* temporarily disable failing test

* custom logic for playlist route to accomodate m3u content type

* incorporate playlist parsing into existing logic in core

* re-enable test

* fix locally failing test

* Address requested changes.

* Improve ImportFile tests.

* Remove ownerID as a parameter of ImportM3U.

* Write tests for ImportM3U.

* Separate ImportM3U test into two.

* Test OwnerID and playlist Name.

---------

Co-authored-by: Sam Watson <SwatsonCodes@users.noreply.github.com>
Co-authored-by: caiocotts <caio@cotts.com.br>
2023-11-01 14:59:47 -04:00
dependabot[bot]
6bca7531aa Bump @babel/traverse from 7.19.3 to 7.23.2 in /ui
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.19.3 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-31 17:46:10 -04:00
dependabot[bot]
68d1d5c99f Bump github.com/lestrrat-go/jwx/v2 from 2.0.12 to 2.0.16
Bumps [github.com/lestrrat-go/jwx/v2](https://github.com/lestrrat-go/jwx) from 2.0.12 to 2.0.16.
- [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.12...v2.0.16)

---
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>
2023-10-31 17:27:45 -04:00
dependabot[bot]
db6c46091e Bump github.com/beego/beego/v2 from 2.0.7 to 2.1.3
Bumps [github.com/beego/beego/v2](https://github.com/beego/beego) from 2.0.7 to 2.1.3.
- [Release notes](https://github.com/beego/beego/releases)
- [Changelog](https://github.com/beego/beego/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/beego/beego/compare/v2.0.7...v2.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-31 17:24:55 -04:00
dependabot[bot]
4cd916bb78 Bump react-router-dom from 5.3.0 to 5.3.4 in /ui
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 5.3.0 to 5.3.4.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/v5.3.4/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-31 16:55:59 -04:00
dependabot[bot]
c40e83efab Bump github.com/go-chi/chi/v5 from 5.0.8 to 5.0.10
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.8 to 5.0.10.
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.0.8...v5.0.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-31 16:44:35 -04:00
Stephan Wahlen
9094f41f25 Improve TopSongs findMatchingTrack by de-prioritizing compilations (#2532)
in reference to https://github.com/navidrome/navidrome/issues/1701
2023-10-31 16:00:53 -04:00
dependabot[bot]
9ff95b6ced Bump github.com/onsi/gomega from 1.27.10 to 1.29.0
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.27.10 to 1.29.0.
- [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.10...v1.29.0)

---
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>
2023-10-31 15:48:16 -04:00
Kendall Garner
77ace8570c Coalesce genre to null for smart playlist (#2573) 2023-10-31 13:22:57 -04:00
Matthias Schmidt
59f0c487e7 Jukebox cleanup (#2554)
* Fixing typo FFmpegPath -> MPVPath

* Fixing panic by applying afontenot patch

* Using mpv audio-device flag and naming for config and playback
2023-10-17 18:12:48 -04:00
Deluan
2cd4358172 Make Jukebox available to Subsonic clients 2023-09-14 20:15:39 -04:00
dependabot[bot]
248bf232ff Bump github.com/lestrrat-go/jwx/v2 from 2.0.11 to 2.0.12 (#2480)
Bumps [github.com/lestrrat-go/jwx/v2](https://github.com/lestrrat-go/jwx) from 2.0.11 to 2.0.12.
- [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.11...v2.0.12)

---
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-09-10 18:06:51 -04:00
dependabot[bot]
b5664ab905 Bump github.com/onsi/ginkgo/v2 from 2.11.0 to 2.12.0 (#2497)
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.11.0 to 2.12.0.
- [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.11.0...v2.12.0)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  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-09-10 12:28:43 -04:00
Lukas H
ac7f94e620 Fix text being unreadable with Ligera theme (#2517)
Change MuiFormGroup color to make it readable.
2023-09-10 12:28:21 -04:00
dependabot[bot]
d45f9f172d Bump github.com/google/uuid from 1.3.0 to 1.3.1 (#2489)
Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/google/uuid/releases)
- [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/uuid/compare/v1.3.0...v1.3.1)

---
updated-dependencies:
- dependency-name: github.com/google/uuid
  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-09-10 12:05:58 -04:00
dependabot[bot]
250107d668 Bump golang.org/x/image from 0.9.0 to 0.12.0 (#2507)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.9.0 to 0.12.0.
- [Commits](https://github.com/golang/image/compare/v0.9.0...v0.12.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-09-10 12:05:28 -04:00
BoniK
64b14db55a Add Korean Language (#2463) 2023-09-10 11:52:18 -04:00
dependabot[bot]
73d1851c0d Bump golang.org/x/tools from 0.9.1 to 0.13.0 (#2516)
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.9.1 to 0.13.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.9.1...v0.13.0)

---
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-09-10 11:48:33 -04:00
Matthias Schmidt
1b16e1140f Jukebox mode (#2289)
* Adding cache directory to ignore-list

* Adding jukebox-related config options

* Adding DevEnableJukebox config option pls. dummy server

* Adding types and routers

* Now without panic

* First draft on parsing the action

* Some cleanups

* Adding playback server

* Verify audio device configuration

* Adding debug-build target to have full symbol support

* Adding beep sound library pls some example code. Not working yet

* Play a fixed mp3 on any interface access for testing purposes

* Put action code into separate file, adding stringer, more debug output, prepare structs, validation

* Put action parameter parser code where it belongs

* Have a single Action transporting all information

* User fmt.Errorf for error-generation

* Adding wide playback interface

* Use action map for parsing, stringer instead switch stmt.

* Use but only one switch case and direct dispatch, refactoring

* Add error handling and pushing to client

* send decent errormessage, no internal server error

* Adding playback devices slice and load it from config

* Combine config-verification and structure init

* Return user-specific device

* Separate playback server from device

* Use dataStore to retrieve mediafile by id

* WIP: Playlist and start/stop handling. Doing start/stop the hard way as of now

* WIP: set, start and stop work on one single song. More to come

* Dont need to wait for the end

* Merge jukebox_action.go into jukebox.go

* Remove getParameterAsInt64(). Use existing requiredParamInt() instead

* Dont need to call newFailure() explicitly

* Remove int64, use int instead.

* Add and set action now accept multiple ids

* Kickout copy of childFromMediaFile(). It is not needed here.

* Refactoring devices and playbackServer

* Turn (internal) playback.DeviceStatus into subsonic JukeboxStatus when rendering output. Indexes int64 -> int

* Now we have a position and playing status

* Switching gain to float32, xs:float is defined as 32 bit. Fixing nasty copy/pointer bug

* Now with volume control

* Start working the queue

* Remove user from device interface

* Rename function GetDevice -> GetDeviceForUser to make intention clearer

* Have a nice stringer for the queue

* User Prepared boolean for now to allow pause/unpause

* Skipping works, but without offsets

* Make ChildFromMediaFile public to be used in jukebox get() implementation

* Return position in seconds and implement offset-skip in seconds

* Default offset to 0

* Adding a simple setGain implementation

* Prepare for transcoding AAC

* WIP: transcode to WAV to use beeps wav decoder. Not done yet.

* WIP: out of sheer desparation: convert to MP3 (which works) rather than WAV to troubleshoot issue.

* Use FLAC as intermediate format to play Apple AAC

* A bit of cleanup

* Catching the end-of-stream event for further reactions

* Have a trackSwitching goroutine waiting on channel when track ends

* Move decoder code into own file. Restructure code a bit

* Now with going on to play the next song in the playlist

* Adding shuffle feature

* Implementing remove action

* Cleanup code

* Remove templates for ffmpeg mp3 generation. Not needed anymore.

* Adding some documentation

* Check whether offset into track is in range. Fixing potential remove track bug. Documentation

* Make golangci-lint happy: handling return values

* Adding test suite and example dummy for playback package

* Adding some basic queue tests

* Only use Jukebox.Enabled config option

* Adding stream closing handling

* Pass context.Context to all PlaybackDevice methods

* Remove unneeded function

* Correct spelling

* Reduce visibility of ChildFromMediaFile

* Decomplicate action-parsing

* Adding simple tempfile-based AAC->FLAC transcoding. No parallel reading and writing yet.

* Try to optimize pipe-writing, tempfile-handling and reading. Not done yet.

* Do a synchronous copy of the tempfile. Racecondition detected

* More debugging statements and fixing the play/pause bug. More work needed

* Start the trackSwitcher() with each device once. Return JSON position even if its 0. More debug-output

* Moving all track-handling code into own module

* Fix typo. Do not pass ctx around when not applicable

* WIP: More refactoring, debugging output

* Fix nil pointer

* Repairing MP3 playback by pinning indirect dependencies: hajimehoshi/go-mp3 and hajimehoshi/oto

* Do not forget to cleanup after a skip action

* Make resync with master easy

* Adding missing mocks

* Adding missing error-handling found by linter

* Updating github.com/hajimehoshi/oto

* Removing duplicate function

* Move BEEP-related code into own package

* Juggle beep-related code around as preparation for interface access

* More refactoring for interface separation

* Gather CloseDevice() behind Track interface.

* Adding skeleton, draft audio-interface using mpv.io

* Adding majority of interface commands using messages to mpv socket.

* Adding end-of-stream handling

* MPV: start/stop are working

* postition is given in float in mpv

* Unify Close() and CloseDevice(). Using temp filename for controlling socket

* Wait until control-socket shows up. Cleanup socket in Close()

* Use canceable command. Rename to Executor

* Skipping tracks works now

* Now with actually setting the position

* Fix regain

* Add missing error-handling found by linter

* Adding retry mode on time-pos property getter

* Remove unneeded code on queue

* Putting build-tag beep onto beep files

* Remove deprecated call to rand.Seed()

"As of Go 1.20 there is no reason to call Seed with a random value. Programs that call Seed with a known value to get a specific sequence of results should use New(NewSource(seed)) to obtain a local random generator."

* Using int32 to conform to Subsonic API spec

* Fix merge error

* Minor style changes

* Get username from context

---------

Co-authored-by: Deluan <deluan@navidrome.org>
2023-09-10 11:25:22 -04:00
Deluan Quintão
f941347cf1 Upgrade to Go 1.21 (#2475)
* Upgrade to Go 1.21

* Remove 'replacements' from goreleaser config
2023-08-09 11:39:49 -04:00
dependabot[bot]
1b5cefdada Bump github.com/onsi/gomega from 1.27.8 to 1.27.9 (#2450)
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.27.8 to 1.27.9.
- [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.8...v1.27.9)

---
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-07-22 19:21:39 -04:00
dependabot[bot]
4cf25fc611 Bump github.com/microcosm-cc/bluemonday from 1.0.24 to 1.0.25 (#2449)
Bumps [github.com/microcosm-cc/bluemonday](https://github.com/microcosm-cc/bluemonday) from 1.0.24 to 1.0.25.
- [Release notes](https://github.com/microcosm-cc/bluemonday/releases)
- [Commits](https://github.com/microcosm-cc/bluemonday/compare/v1.0.24...v1.0.25)

---
updated-dependencies:
- dependency-name: github.com/microcosm-cc/bluemonday
  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-07-22 19:11:15 -04:00
dependabot[bot]
14ba83ea1b Bump github.com/go-chi/chi/v5 from 5.0.8 to 5.0.10 (#2444)
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.8 to 5.0.10.
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.0.8...v5.0.10)

---
updated-dependencies:
- dependency-name: github.com/go-chi/chi/v5
  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-07-21 19:12:56 -04:00
dependabot[bot]
08f3fd1343 Bump github.com/pressly/goose/v3 from 3.13.1 to 3.13.4 (#2442)
Bumps [github.com/pressly/goose/v3](https://github.com/pressly/goose) from 3.13.1 to 3.13.4.
- [Release notes](https://github.com/pressly/goose/releases)
- [Changelog](https://github.com/pressly/goose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pressly/goose/compare/v3.13.1...v3.13.4)

---
updated-dependencies:
- dependency-name: github.com/pressly/goose/v3
  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>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2023-07-21 19:11:59 -04:00
dependabot[bot]
3d66f58725 Bump tough-cookie from 4.1.2 to 4.1.3 in /ui (#2441)
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.1.2...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-21 19:04:32 -04:00
dependabot[bot]
5b1ba3df05 Bump word-wrap from 1.2.3 to 1.2.4 in /ui (#2446)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-21 19:02:10 -04:00
Deluan
a002830775 Fix EnableMediaFileCoverArt option default value 2023-07-10 18:07:58 -04:00
dependabot[bot]
7b600bed05 Bump golang.org/x/tools from 0.10.0 to 0.11.0 (#2432)
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.10.0 to 0.11.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.10.0...v0.11.0)

---
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-07-05 16:38:32 -04:00
dependabot[bot]
7d0a1916d8 Bump golang.org/x/image from 0.8.0 to 0.9.0 (#2430)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.8.0 to 0.9.0.
- [Commits](https://github.com/golang/image/compare/v0.8.0...v0.9.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-07-05 16:34:58 -04:00
dependabot[bot]
c7fe311c7f Bump github.com/go-chi/jwtauth/v5 from 5.1.0 to 5.1.1 (#2427)
Bumps [github.com/go-chi/jwtauth/v5](https://github.com/go-chi/jwtauth) from 5.1.0 to 5.1.1.
- [Release notes](https://github.com/go-chi/jwtauth/releases)
- [Commits](https://github.com/go-chi/jwtauth/compare/v5.1.0...v5.1.1)

---
updated-dependencies:
- dependency-name: github.com/go-chi/jwtauth/v5
  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-07-05 16:31:38 -04:00
dependabot[bot]
4520a34648 Bump github.com/pressly/goose/v3 from 3.11.2 to 3.13.1 (#2428)
Bumps [github.com/pressly/goose/v3](https://github.com/pressly/goose) from 3.11.2 to 3.13.1.
- [Release notes](https://github.com/pressly/goose/releases)
- [Changelog](https://github.com/pressly/goose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pressly/goose/compare/v3.11.2...v3.13.1)

---
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-07-05 16:31:22 -04:00
BenzLeung
3e14c3c4f8 Add support for lyrics tag unsynced lyrics (#2391)
* Add support for lyrics tag `unsynced_lyrics`

* Update metadata.go

* Update metadata.go

resolve lint issue

* format the code with `goimports`

format the code with `goimports`
2023-06-20 09:32:49 -04:00
dependabot[bot]
1e891d6b07 Bump github.com/prometheus/client_golang from 1.15.1 to 1.16.0 (#2408)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.15.1 to 1.16.0.
- [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.15.1...v1.16.0)

---
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-06-19 14:23:36 -04:00
dependabot[bot]
caf9b22d35 Bump golang.org/x/image from 0.7.0 to 0.8.0 (#2407)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.7.0 to 0.8.0.
- [Commits](https://github.com/golang/image/compare/v0.7.0...v0.8.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-06-19 14:23:01 -04:00
Deluan Quintão
4f8742bcd1 Update translations (#2329)
Co-authored-by: deluan <deluan@users.noreply.github.com>
2023-06-19 12:27:56 -04:00
Deluan
26aa0f4fff Fix typo 2023-06-19 11:19:43 -04:00
Deluan
4898f31f6d Add format target to Makefile 2023-06-19 11:15:51 -04:00
Philipp Wolfer
9da013f339 Submit duration to ListenBrainz (#2405) 2023-06-17 12:27:00 -04:00
Deluan
5af67c78af Upgrade Go dependencies 2023-06-16 22:34:02 -04:00
Philipp Wolfer
c8608956be Fix listenbrainz submission and clarify MusicBrainz recording ID field (#2279)
* Fix MB recording ID parameter name for ListenBrainz submission

This follows the ListenBrainz API documentation.

Fixes #1657

* Rename MediaFile.MbzTrackID to MbzRecordingID

This better reflects the actual data. That the MusicBrainz
recording ID is stored in file metadata as musicbrainz_trackid
is a historical artifact.

* Rename database column mbz_track_id to mbz_recording_id
2023-06-16 18:00:01 -04:00
Deluan
36eda871f6 Fix locale-dependent tests. Closes #2402 2023-06-16 16:38:03 -04:00
David Casado
7c92a73208 Ignore playlists starting with a dot - #2367 (#2390) 2023-06-16 15:55:17 -04:00
Deluan
f5d97823e8 Fix original date (TDOR) mapping for ffmpeg extractor 2023-06-06 19:13:45 -04:00
Deluan
d6083dab6e Re-apply "Refactor walkDirTree to use fs.FS" but remove context cancelation logic.
This reverts commit 6b3b4d83ff.
2023-06-04 15:06:19 -04:00
Deluan
6b3b4d83ff Revert "Refactor walkDirTree to use fs.FS"
This reverts commit 3853c3318f.
2023-06-04 14:13:33 -04:00
Deluan
3853c3318f Refactor walkDirTree to use fs.FS 2023-06-03 22:25:19 -04:00
tomleb
257ccc5f43 Allow configuring cache folder (#2357)
* Set all clients to dev_download for make get-music

* Use multiple TranscodingCache instances in tests

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

* Allow configuring cache folder

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

* Fix tests

* Removed unused test setup code

---------

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

* Update AlbumDetails.js

* Update AlbumDetails.js

* Create DoubleRangeField.js

* Update and rename DoubleRangeField.js to RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update AlbumGridView.js

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update index.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update en.json

* Update en.json

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update AlbumGridView.js

* Update AlbumDetails.js

* Update AlbumSongs.js

* Update ContextMenus.js

* Update SongDatagrid.js

* Update AlbumSongs.js

* Update SongDatagrid.js

* Update SongDatagrid.js

* Update SongDatagrid.js

* Update AlbumSongs.js

* Update SongList.js

* Update playlist_track_repository.go

* Update 20230113000000_release_year.go

* Update PlayButton.js

* Update mediafile_repository.go

* Update album.go

* Update playlist_track_repository.go

* Update playlist_track_repository.go

* Update SongDatagrid.js

* Update 20230113000000_release_year.go

* Update SongDatagrid.js

* Update AlbumSongs.js

* Update SongDatagrid.js

* Update SongDatagrid.js

* Update SongDatagrid.js

* Update SongDatagrid.js

* Update AlbumDetails.js

* Update AlbumSongs.js

* Update AlbumSongs.js

* Update RangeFieldDouble.js

* Update SongDatagrid.js

* Update 20230113000000_release_year.go

* Update 20230113000000_release_year.go

* Update 20230113000000_release_year.go

* Update 20230113000000_release_year.go

* Update AlbumSongs.js

* Update AlbumSongs.js

* Update mapping.go

* Update RangeFieldDouble.js

* Update AlbumGridView.js

* Update AlbumSongs.js

* Update en.json

* Update SongDatagrid.js

* Update SongDatagrid.js

* Update metadata.go

* Update mapping.go

* Update AlbumDetails.js

* Update AlbumGridView.js

* Update RangeFieldDouble.js

* Update mapping.go

* Update metadata.go

* Update mapping.go

* Update AlbumDetails.js

* Update 20230113000000_release_year.go

* Update AlbumDetails.js

* Update en.json

* Update configuration.go

* Update mapping.go

* Update configuration.go

* Update mediafile.go

* Update metadata.go

* Update RangeFieldDouble.js

* Update 20230113000000_release_year.go

* Update configuration.go

* Update mapping.go

* Update mediafile.go

* Update mapping.go

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update 20230113000000_release_year.go

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update mapping.go

* Update metadata.go

* Update album.go

* Update mediafile.go

* Update mediafile.go

* Update album.go

* Update fields.go

* Update mediafile_repository.go

* Update playlist_track_repository.go

* Update AlbumSongs.js

* Update SongDatagrid.js

* Update PlayButton.js

* Update SongList.js

* Update ContextMenus.js

* Update SongDatagrid.js

* Update metadata.go

* Update ArtistShow.js

* Update mapping.go

* Update configuration.go

* Update mapping.go

* Update metadata.go

* Update metadata.go

* Update mapping.go

* Update metadata.go

* Update metadata.go

* Update mapping.go

* Update 20230113000000_release_year.go

* Update 20230113000000_release_year.go

* Update mapping.go

* Update metadata.go

* Update metadata.go

* Update album.go

* Update mediafile.go

* Update AlbumDetails.js

* Update AlbumSongs.js

* Update album.go

* Update mediafile.go

* Update metadata.go

* Update mediafile.go

* Update 20230113000000_release_year.go

* Update 20230113000000_release_year.go

* Update album.go

* Update mediafile.go

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update AlbumGridView.js

* Update en.json

* Update AlbumGridView.js

* Update RangeFieldDouble.js

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

* Update album.go

* Update mediafile.go

* Update fields.go

* Update playlist_track_repository.go

* Update mediafile_repository.go

* Update mapping.go

* Update metadata.go

* Update mapping.go

* Update SongDatagrid.js

* Update RangeFieldDouble.js

* Update index.js

* Update ContextMenus.js

* Update PlayButton.js

* Create FormatDate.js

* Update SongList.js

* Update AlbumDetails.js

* Update AlbumSongs.js

* Update AlbumSongs.js

* Update en.json

* Update AlbumDetails.js

* Update album.go

fixed conflict I think?

* Update mediafile.go

fixed conflict

* Format with goimports

* Update SongDatagrid.js

only show Cat # in desktop view

* Update metadata_internal_test.go

* Update metadata_test.go

* Delete test.mp3

* Add files via upload

mp3 test file with Date, Original Date and Release Date

* Update metadata_test.go

* Update metadata_test.go

* Update metadata_test.go

* Update metadata_test.go

* Update taglib_test.go

* Delete test.mp3

* Add files via upload

file with replaygain & dates

* Update AlbumGridView.js

* Update AlbumDetails.js

* Update AlbumSongs.js

* Update ContextMenus.js

* Update FormatDate.js

* Update PlayButton.js

* Update RangeFieldDouble.js

* Update SongDatagrid.js

* Update AlbumSongs.js

* Update SongDatagrid.js

* Update AlbumSongs.js

* Fix formatting

* Update mapping.go

* Update AlbumSongs.js

* Update SongDatagrid.js

* Update SongDatagrid.js

prettier

* Create RangeDoubleField.js

rename of RangeFieldDouble.js

* Update AlbumGridView.js

RangeFieldDouble -> RangeDoubleField

* Update mediafile.go

AllOrNothing() -> allOrNothing()

* Update metadata_internal_test.go

getYear -> getDate

* Update AlbumDetails.js

wrote suggested changes

* Update en.json

Editions -> Releases & fixed the field name

* Update configuration.go

Rename Editions -> Releases

* Update 20230113000000_release_date.go

Editions -> Releases

* Update album.go

Editions -> Releases

* Update mediafile.go

Editions -> Releases

* Update AlbumDetails.js

Editions -> Releases

* Update AlbumSongs.js

Editions -> Releases

* Update RangeDoubleField.js

Editions -> Releases

* Update SongDatagrid.js

Editions -> Releases

* Update index.js

FormatFullDate and RangeDoubleField

* Rename FormatDate.js to FormatFullDate.js

* Delete RangeFieldDouble.js

* Update mediafile.go

AllOrNothing -> allOrNothing

* Update mapping.go

Editions -> Releases

* Update AlbumDetails.js

prettier

* Update SongDatagrid.js

showReleaseRow -> showReleaseDivider

* Update AlbumSongs.js

showReleaseRow -> showReleaseDivider for clarity

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

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

* Update 20230515184510_add_release_date.go

* Update fields.go

* Update album.go

* Update mediafile.go

* Update mapping.go

* Update AlbumDetails.js

* Update en.json

* Update AlbumDetails.js

* Update AlbumDetails.js

now hopefully prettier

* Update mapping.go

---------

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

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

* Add replaygain tests for m4a, mp4, ogg

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Fixed issue no #2174

---------

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

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

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

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

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

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

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

* Update Chinese translations

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

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

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

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

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

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

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

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

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

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

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

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

* expand iff

---------

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

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

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

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

* Fix ArtistID3

* Fix AlbumID3

* Fix Child

* Fix NowPlayingEntry

* Fix Playlist

* Fix Share

* Fix User

* Fix Artist

* Fix Directory

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

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

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

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

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

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

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

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

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

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

View File

@@ -2,7 +2,7 @@
# [Choice] Go version: 1, 1.15, 1.14
ARG VARIANT="1"
FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT}
FROM mcr.microsoft.com/vscode/devcontainers/go:${VARIANT}
# [Option] Install Node.js
ARG INSTALL_NODE="true"
@@ -17,4 +17,4 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# RUN go get -x <your-dependency-or-tool>
# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

@@ -16,11 +16,13 @@ RUN chmod +x /navidrome
#####################################################
### Build Final Image
FROM alpine as release
FROM alpine:3.18
LABEL maintainer="deluan@navidrome.org"
# Install ffmpeg and output build config
RUN apk add --no-cache ffmpeg
# Install ffmpeg and mpv
RUN apk add -U --no-cache ffmpeg mpv
# Show ffmpeg build info, for troubleshooting purposes
RUN ffmpeg -buildconf
COPY --from=copy-binary /navidrome /app/

View File

@@ -1,4 +1,4 @@
name: Pipeline
name: "Pipeline: Test, Lint, Build"
on:
push:
branches:
@@ -8,30 +8,28 @@ on:
pull_request:
branches:
- master
jobs:
go-lint:
name: Lint Go code
runs-on: ubuntu-latest
container: deluan/ci-goreleaser:1.22.3-1
steps:
- name: Install taglib
run: sudo apt-get install libtag1-dev
- uses: actions/checkout@v4
- name: Set up Go 1.19
uses: actions/setup-go@v3
with:
go-version: 1.19
- uses: actions/checkout@v3
- name: Config workspace folder as trusted
run: git config --global --add safe.directory $GITHUB_WORKSPACE; git describe --dirty --always --tags
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v6
with:
version: latest
github-token: ${{ secrets.GITHUB_TOKEN }}
problem-matchers: true
args: --timeout 2m
- name: Install goimports
run: go install golang.org/x/tools/cmd/goimports
run: go install golang.org/x/tools/cmd/goimports@latest
- run: goimports -w `find . -name '*.go' | grep -v '_gen.go$'`
- run: go mod tidy
@@ -39,28 +37,20 @@ 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 "make format" and commit the changes'
exit 1
fi
go:
name: Test with Go ${{ matrix.go_version }}
name: Test Go code
runs-on: ubuntu-latest
strategy:
matrix:
go_version: [1.20.x,1.19.x]
container: deluan/ci-goreleaser:1.22.3-1
steps:
- name: Install taglib
run: sudo apt-get install libtag1-dev
- name: Check out code into the Go module directory
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up Go ${{ matrix.go_version }}
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go_version }}
cache: true
- name: Config workspace folder as trusted
run: git config --global --add safe.directory $GITHUB_WORKSPACE; git describe --dirty --always --tags
- name: Download dependencies
if: steps.cache-go.outputs.cache-hit != 'true'
@@ -69,20 +59,20 @@ 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
runs-on: ubuntu-latest
env:
NODE_OPTIONS: '--max_old_space_size=4096'
NODE_OPTIONS: "--max_old_space_size=4096"
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 16
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: 20
cache: "npm"
cache-dependency-path: "**/package-lock.json"
- name: npm install dependencies
run: |
@@ -104,7 +94,7 @@ jobs:
cd ui
npm run build
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: js-bundle
path: ui/build
@@ -114,41 +104,34 @@ jobs:
name: Build binaries
needs: [js, go, go-lint]
runs-on: ubuntu-latest
container: deluan/ci-goreleaser:1.22.3-1
steps:
- name: Checkout Code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/download-artifact@v3
- name: Config workspace folder as trusted
run: git config --global --add safe.directory $GITHUB_WORKSPACE; git describe --dirty --always --tags
- uses: actions/download-artifact@v4
with:
name: js-bundle
path: ui/build
- name: Config /github/workspace folder as trusted
uses: docker://deluan/ci-goreleaser:1.19.5-1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: /bin/bash -c "git config --global --add safe.directory /github/workspace; git describe --dirty --always --tags"
- name: Run GoReleaser - SNAPSHOT
if: startsWith(github.ref, 'refs/tags/') != true
uses: docker://deluan/ci-goreleaser:1.19.5-1
run: goreleaser release --clean --skip=publish --snapshot
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: goreleaser release --rm-dist --skip-publish --snapshot
- name: Run GoReleaser - RELEASE
if: startsWith(github.ref, 'refs/tags/')
uses: docker://deluan/ci-goreleaser:1.19.5-1
run: goreleaser release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: goreleaser release --rm-dist
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: binaries
path: |
@@ -158,7 +141,7 @@ jobs:
retention-days: 7
docker:
name: Build Docker images
name: Build and publish Docker images
needs: [binaries]
runs-on: ubuntu-latest
env:
@@ -166,28 +149,59 @@ jobs:
steps:
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
if: env.DOCKER_IMAGE != ''
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
if: env.DOCKER_IMAGE != ''
- uses: actions/checkout@v3
- uses: actions/checkout@v4
if: env.DOCKER_IMAGE != ''
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
if: env.DOCKER_IMAGE != ''
with:
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@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: env.DOCKER_IMAGE != ''
uses: docker/login-action@v3
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@v5
with:
labels: |
maintainer=deluan
images: |
name=${{secrets.DOCKER_IMAGE}}
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@v5
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 }}

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

@@ -0,0 +1,56 @@
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@v5
with:
process-only: 'issues, prs'
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@v9
with:
operations-per-run: 999
days-before-issue-stale: 180
days-before-pr-stale: 180
days-before-issue-close: 30
days-before-pr-close: 30
stale-issue-message: >
This issue has been automatically marked as stale because it has not had
recent activity. The resources of the Navidrome team are limited, and so we are asking for your help.
If this is a **bug** and you can still reproduce this error on the <code>master</code> branch, please reply with all of the information you have about it in order to keep the issue open.
If this is a **feature request**, and you feel that it is still relevant and valuable, please tell us why.
This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
stale-pr-message: This PR has been automatically marked as stale because it has not had
recent activity. The resources of the Navidrome team are limited, and so we are asking for your help.
Please check https://github.com/navidrome/navidrome/blob/master/CONTRIBUTING.md#pull-requests and verify that this code contribution fits with the description. If yes, tell it in a comment.
This PR will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
stale-issue-label: 'stale'
exempt-issue-labels: 'keep,security'
stale-pr-label: 'stale'
exempt-pr-labels: 'keep,security'

View File

@@ -1,4 +1,4 @@
name: Update Translations
name: POEditor import
on:
workflow_dispatch:
schedule:
@@ -6,16 +6,21 @@ on:
jobs:
update-translations:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'navidrome' }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Get updated translations
env:
POEDITOR_PROJECTID: ${{ secrets.POEDITOR_PROJECTID }}
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
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.PAT }}
commit-message: Update translations

5
.gitignore vendored
View File

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

View File

@@ -1,13 +1,9 @@
run:
go: "1.19"
linters:
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- depguard
- dogsled
- durationcheck
- errcheck
@@ -29,8 +25,9 @@ linters:
- unused
- whitespace
issues:
exclude-rules:
- linters:
- gosec
text: "(G501|G401|G505):"
linters-settings:
gosec:
excludes:
- G501
- G401
- G505

View File

@@ -116,12 +116,6 @@ archives:
- format_overrides:
- goos: windows
format: zip
replacements:
darwin: macOS
linux: Linux
windows: Windows
386: i386
amd64: x86_64
checksum:
name_template: "{{ .ProjectName }}_checksums.txt"

2
.nvmrc
View File

@@ -1 +1 @@
v16
v20

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.22.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,36 @@ 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
format: ##@Development Format code
@(cd ./ui && npm run prettier)
@go run golang.org/x/tools/cmd/goimports@latest -w `find . -name '*.go' | grep -v _gen.go$$`
@go mod tidy
.PHONY: format
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/migrations 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/migrations create ${name}
.PHONY: migration
setup-dev: setup
@@ -73,13 +85,18 @@ build: warning-noui-build check_go_env ##@Build Build only backend
go build -ldflags="-X github.com/navidrome/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/navidrome/navidrome/consts.gitTag=$(GIT_TAG)-SNAPSHOT" -tags=netgo
.PHONY: build
debug-build: warning-noui-build check_go_env ##@Build Build only backend (with remote debug on)
go build -gcflags="all=-N -l" -ldflags="-X github.com/navidrome/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/navidrome/navidrome/consts.gitTag=$(GIT_TAG)-SNAPSHOT" -tags=netgo
.PHONY: debug-build
buildjs: check_node_env ##@Build Build only frontend
@(cd ./ui && npm run build)
.PHONY: buildjs
all: warning-noui-build ##@Cross_Compilation Build binaries for all supported platforms. It does not build the frontend
@echo "Building binaries for all platforms using builder ${CI_RELEASER_VERSION}"
docker run -t -v $(PWD):/workspace -w /workspace deluan/ci-goreleaser:$(CI_RELEASER_VERSION) \
goreleaser release --rm-dist --skip-publish --snapshot
goreleaser release --clean --skip=publish --snapshot
.PHONY: all
single: warning-noui-build ##@Cross_Compilation Build binaries for a single supported platforms. It does not build the frontend
@@ -89,11 +106,17 @@ single: warning-noui-build ##@Cross_Compilation Build binaries for a single supp
grep -- "- id: navidrome_" .goreleaser.yml | sed 's/- id: navidrome_//g'; \
exit 1; \
fi
@echo "Building binaries for ${GOOS}/${GOARCH}"
@echo "Building binaries for ${GOOS}/${GOARCH} using builder ${CI_RELEASER_VERSION}"
docker run -t -v $(PWD):/workspace -e GOOS -e GOARCH -w /workspace deluan/ci-goreleaser:$(CI_RELEASER_VERSION) \
goreleaser build --rm-dist --snapshot --single-target --id navidrome_${GOOS}_${GOARCH}
goreleaser build --clean --snapshot -p 2 --single-target --id navidrome_${GOOS}_${GOARCH}
.PHONY: single
docker: buildjs ##@Build Build Docker linux/amd64 image (tagged as `deluan/navidrome:develop`)
GOOS=linux GOARCH=amd64 make single
@echo "Building Docker image"
docker build . --platform linux/amd64 -t deluan/navidrome:develop -f .github/workflows/pipeline.dockerfile
.PHONY: docker
warning-noui-build:
@echo "WARNING: This command does not build the frontend, it uses the latest built with 'make buildjs'"
.PHONY: warning-noui-build
@@ -102,9 +125,9 @@ get-music: ##@Development Download some free music from Navidrome's demo instanc
mkdir -p music
( cd music; \
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=dev_download&id=ec2093ec4801402f1e17cc462195cdbb" > brock.zip; \
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=NavidromeUI&id=b376eeb4652d2498aa2b25ba0696725e" > back_on_earth.zip; \
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=NavidromeUI&id=e49c609b542fc51899ee8b53aa858cb4" > ugress.zip; \
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=NavidromeUI&id=350bcab3a4c1d93869e39ce496464f03" > voodoocuts.zip; \
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=dev_download&id=b376eeb4652d2498aa2b25ba0696725e" > back_on_earth.zip; \
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=dev_download&id=e49c609b542fc51899ee8b53aa858cb4" > ugress.zip; \
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=dev_download&id=350bcab3a4c1d93869e39ce496464f03" > voodoocuts.zip; \
for file in *.zip; do unzip -n $${file}; done )
@echo "Done. Remember to set your MusicFolder to ./music"
.PHONY: get-music

View File

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

View File

@@ -1,13 +1,13 @@
<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)
[![Downloads](https://img.shields.io/github/downloads/navidrome/navidrome/total?logo=github&style=flat-square)](https://github.com/navidrome/navidrome/releases/latest)
[![Docker Pulls](https://img.shields.io/docker/pulls/deluan/navidrome?logo=docker&label=pulls&style=flat-square)](https://hub.docker.com/r/deluan/navidrome)
[![Dev Chat](https://img.shields.io/discord/671335427726114836?logo=discord&label=discord&style=flat-square)](https://discord.gg/xh7j7yF)
[![Subreddit](https://img.shields.io/reddit/subreddit-subscribers/navidrome?logo=reddit&label=/r/navidrome&style=flat-square)](https://www.reddit.com/r/navidrome/)
[![Subreddit](https://img.shields.io/badge/%2Fr%2Fnavidrome-%2B3000-red?logo=reddit)](https://www.reddit.com/r/navidrome/)
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0-ff69b4.svg?style=flat-square)](CODE_OF_CONDUCT.md)
Navidrome is an open source web-based music collection server and streamer. It gives you freedom to listen to your
@@ -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

99
cmd/inspect.go Normal file
View File

@@ -0,0 +1,99 @@
package cmd
import (
"encoding/json"
"fmt"
"strings"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/scanner"
"github.com/navidrome/navidrome/scanner/metadata"
"github.com/navidrome/navidrome/tests"
"github.com/pelletier/go-toml/v2"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
var (
extractor string
format string
)
func init() {
inspectCmd.Flags().StringVarP(&extractor, "extractor", "x", "", "extractor to use (ffmpeg or taglib, default: auto)")
inspectCmd.Flags().StringVarP(&format, "format", "f", "pretty", "output format (pretty, toml, yaml, json, jsonindent)")
rootCmd.AddCommand(inspectCmd)
}
var inspectCmd = &cobra.Command{
Use: "inspect [files to inspect]",
Short: "Inspect tags",
Long: "Show file tags as seen by Navidrome",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
runInspector(args)
},
}
var marshalers = map[string]func(interface{}) ([]byte, error){
"pretty": prettyMarshal,
"toml": toml.Marshal,
"yaml": yaml.Marshal,
"json": json.Marshal,
"jsonindent": func(v interface{}) ([]byte, error) {
return json.MarshalIndent(v, "", " ")
},
}
func prettyMarshal(v interface{}) ([]byte, error) {
out := v.([]inspectorOutput)
var res strings.Builder
for i := range out {
res.WriteString(fmt.Sprintf("====================\nFile: %s\n\n", out[i].File))
t, _ := toml.Marshal(out[i].RawTags)
res.WriteString(fmt.Sprintf("Raw tags:\n%s\n\n", t))
t, _ = toml.Marshal(out[i].MappedTags)
res.WriteString(fmt.Sprintf("Mapped tags:\n%s\n\n", t))
}
return []byte(res.String()), nil
}
type inspectorOutput struct {
File string
RawTags metadata.ParsedTags
MappedTags model.MediaFile
}
func runInspector(args []string) {
if extractor != "" {
conf.Server.Scanner.Extractor = extractor
}
log.Info("Using extractor", "extractor", conf.Server.Scanner.Extractor)
md, err := metadata.Extract(args...)
if err != nil {
log.Fatal("Error extracting tags", err)
}
mapper := scanner.NewMediaFileMapper(conf.Server.MusicFolder, &tests.MockedGenreRepo{})
marshal := marshalers[format]
if marshal == nil {
log.Fatal("Invalid format", "format", format)
}
var out []inspectorOutput
for k, v := range md {
if !model.IsAudioFile(k) {
continue
}
if len(v.Tags) == 0 {
continue
}
out = append(out, inspectorOutput{
File: k,
RawTags: v.Tags,
MappedTags: mapper.ToMediaFile(v),
})
}
data, _ := marshal(out)
fmt.Println(string(data))
}

View File

@@ -2,12 +2,14 @@ package cmd
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/go-chi/chi/v5/middleware"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core"
@@ -17,14 +19,11 @@ import (
"github.com/navidrome/navidrome/scheduler"
"github.com/navidrome/navidrome/server/backgrounds"
"github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/sync/errgroup"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/sync/errgroup"
)
var interrupted = errors.New("service was interrupted")
var (
cfgFile string
noBanner bool
@@ -40,10 +39,14 @@ Complete documentation is available at https://www.navidrome.org/docs`,
Run: func(cmd *cobra.Command, args []string) {
runNavidrome()
},
PostRun: func(cmd *cobra.Command, args []string) {
postRun()
},
Version: consts.Version,
}
)
// Execute runs the root cobra command, which will start the Navidrome server by calling the runNavidrome function.
func Execute() {
rootCmd.SetVersionTemplate(`{{println .Version}}`)
if err := rootCmd.Execute(); err != nil {
@@ -59,26 +62,42 @@ func preRun() {
conf.Load()
}
func runNavidrome() {
db.EnsureLatestVersion()
defer func() {
if err := db.Close(); err != nil {
log.Error("Error closing DB", err)
}
log.Info("Navidrome stopped, bye.")
}()
func postRun() {
log.Info("Navidrome stopped, bye.")
}
g, ctx := errgroup.WithContext(context.Background())
// runNavidrome is the main entry point for the Navidrome server. It starts all the services and blocks.
// If any of the services returns an error, it will log it and exit. If the process receives a signal to exit,
// it will cancel the context and exit gracefully.
func runNavidrome() {
defer db.Init()()
ctx, cancel := mainContext()
defer cancel()
g, ctx := errgroup.WithContext(ctx)
g.Go(startServer(ctx))
g.Go(startSignaler(ctx))
g.Go(startSignaller(ctx))
g.Go(startScheduler(ctx))
g.Go(startPlaybackServer(ctx))
g.Go(schedulePeriodicScan(ctx))
if err := g.Wait(); err != nil && !errors.Is(err, interrupted) {
if err := g.Wait(); err != nil {
log.Error("Fatal error in Navidrome. Aborting", err)
}
}
// mainContext returns a context that is cancelled when the process receives a signal to exit.
func mainContext() (context.Context, context.CancelFunc) {
return signal.NotifyContext(context.Background(),
os.Interrupt,
syscall.SIGHUP,
syscall.SIGTERM,
syscall.SIGABRT,
)
}
// startServer starts the Navidrome web server, adding all the necessary routers.
func startServer(ctx context.Context) func() error {
return func() error {
a := CreateServer(conf.Server.MusicFolder)
@@ -96,13 +115,17 @@ 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)
}
}
// schedulePeriodicScan schedules a periodic scan of the music library, if configured.
func schedulePeriodicScan(ctx context.Context) func() error {
return func() error {
schedule := conf.Server.ScanSchedule
@@ -132,16 +155,30 @@ func schedulePeriodicScan(ctx context.Context) func() error {
}
}
// startScheduler starts the Navidrome scheduler, which is used to run periodic tasks.
func startScheduler(ctx context.Context) func() error {
log.Info(ctx, "Starting scheduler")
schedulerInstance := scheduler.GetInstance()
return func() error {
log.Info(ctx, "Starting scheduler")
schedulerInstance := scheduler.GetInstance()
schedulerInstance.Run(ctx)
return nil
}
}
// startPlaybackServer starts the Navidrome playback server, if configured.
// It is responsible for the Jukebox functionality
func startPlaybackServer(ctx context.Context) func() error {
return func() error {
if !conf.Server.Jukebox.Enabled {
log.Debug("Jukebox is DISABLED")
return nil
}
log.Info(ctx, "Starting Jukebox service")
playbackInstance := GetPlaybackServer()
return playbackInstance.Run(ctx)
}
}
// TODO: Implement some struct tags to map flags to viper
func init() {
cobra.OnInitialize(func() {
@@ -151,22 +188,29 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "configfile", "c", "", `config file (default "./navidrome.toml")`)
rootCmd.PersistentFlags().BoolVarP(&noBanner, "nobanner", "n", false, `don't show banner`)
rootCmd.PersistentFlags().String("musicfolder", viper.GetString("musicfolder"), "folder where your music is stored")
rootCmd.PersistentFlags().String("datafolder", viper.GetString("datafolder"), "folder to store application data (DB, cache...), needs write access")
rootCmd.PersistentFlags().String("datafolder", viper.GetString("datafolder"), "folder to store application data (DB), needs write access")
rootCmd.PersistentFlags().String("cachefolder", viper.GetString("cachefolder"), "folder to store cache data (transcoding, images...), needs write access")
rootCmd.PersistentFlags().StringP("loglevel", "l", viper.GetString("loglevel"), "log level, possible values: error, info, debug, trace")
_ = viper.BindPFlag("musicfolder", rootCmd.PersistentFlags().Lookup("musicfolder"))
_ = viper.BindPFlag("datafolder", rootCmd.PersistentFlags().Lookup("datafolder"))
_ = viper.BindPFlag("cachefolder", rootCmd.PersistentFlags().Lookup("cachefolder"))
_ = viper.BindPFlag("loglevel", rootCmd.PersistentFlags().Lookup("loglevel"))
rootCmd.Flags().StringP("address", "a", viper.GetString("address"), "IP address to bind")
rootCmd.Flags().IntP("port", "p", viper.GetInt("port"), "HTTP port Navidrome will use")
rootCmd.Flags().StringP("address", "a", viper.GetString("address"), "IP address to bind to")
rootCmd.Flags().IntP("port", "p", viper.GetInt("port"), "HTTP port Navidrome will listen to")
rootCmd.Flags().String("baseurl", viper.GetString("baseurl"), "base URL to configure Navidrome behind a proxy (ex: /music or http://my.server.com)")
rootCmd.Flags().String("tlscert", viper.GetString("tlscert"), "optional path to a TLS cert file (enables HTTPS listening)")
rootCmd.Flags().String("unixsocketperm", viper.GetString("unixsocketperm"), "optional file permission for the unix socket")
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")
rootCmd.Flags().String("imagecachesize", viper.GetString("imagecachesize"), "size of image (art work) cache. set to 0 to disable cache")
rootCmd.Flags().String("albumplaycountmode", viper.GetString("albumplaycountmode"), "how to compute playcount for albums. absolute (default) or normalized")
rootCmd.Flags().Bool("autoimportplaylists", viper.GetBool("autoimportplaylists"), "enable/disable .m3u playlist auto-import`")
rootCmd.Flags().Bool("prometheus.enabled", viper.GetBool("prometheus.enabled"), "enable/disable prometheus metrics endpoint`")
@@ -174,9 +218,13 @@ 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("unixsocketperm", rootCmd.Flags().Lookup("unixsocketperm"))
_ = viper.BindPFlag("tlskey", rootCmd.Flags().Lookup("tlskey"))
_ = viper.BindPFlag("baseurl", rootCmd.Flags().Lookup("baseurl"))
_ = viper.BindPFlag("sessiontimeout", rootCmd.Flags().Lookup("sessiontimeout"))
_ = viper.BindPFlag("scaninterval", rootCmd.Flags().Lookup("scaninterval"))
_ = viper.BindPFlag("baseurl", rootCmd.Flags().Lookup("baseurl"))
_ = viper.BindPFlag("uiloginbackgroundurl", rootCmd.Flags().Lookup("uiloginbackgroundurl"))
_ = viper.BindPFlag("prometheus.enabled", rootCmd.Flags().Lookup("prometheus.enabled"))

View File

@@ -1,27 +0,0 @@
//go:build windows || plan9
package cmd
import (
"context"
"os"
"os/signal"
"github.com/navidrome/navidrome/log"
)
func startSignaler(ctx context.Context) func() error {
log.Info(ctx, "Starting signaler")
return func() error {
var sigChan = make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
select {
case sig := <-sigChan:
log.Info(ctx, "Received termination signal", "signal", sig)
return interrupted
case <-ctx.Done():
return nil
}
}
}

14
cmd/signaller_nounix.go Normal file
View File

@@ -0,0 +1,14 @@
//go:build windows || plan9
package cmd
import (
"context"
)
// Windows and Plan9 don't support SIGUSR1, so we don't need to start a signaler
func startSignaller(ctx context.Context) func() error {
return func() error {
return nil
}
}

View File

@@ -14,28 +14,17 @@ import (
const triggerScanSignal = syscall.SIGUSR1
func startSignaler(ctx context.Context) func() error {
func startSignaller(ctx context.Context) func() error {
log.Info(ctx, "Starting signaler")
scanner := GetScanner()
return func() error {
var sigChan = make(chan os.Signal, 1)
signal.Notify(
sigChan,
os.Interrupt,
triggerScanSignal,
syscall.SIGHUP,
syscall.SIGTERM,
syscall.SIGABRT,
)
signal.Notify(sigChan, triggerScanSignal)
for {
select {
case sig := <-sigChan:
if sig != triggerScanSignal {
log.Info(ctx, "Received termination signal", "signal", sig)
return interrupted
}
log.Info(ctx, "Received signal, triggering a new scan", "signal", sig)
start := time.Now()
err := scanner.RescanAll(ctx, false)

View File

@@ -1,6 +1,6 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
@@ -14,6 +14,7 @@ import (
"github.com/navidrome/navidrome/core/agents/listenbrainz"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/ffmpeg"
"github.com/navidrome/navidrome/core/playback"
"github.com/navidrome/navidrome/core/scrobbler"
"github.com/navidrome/navidrome/db"
"github.com/navidrome/navidrome/persistence"
@@ -23,7 +24,6 @@ import (
"github.com/navidrome/navidrome/server/nativeapi"
"github.com/navidrome/navidrome/server/public"
"github.com/navidrome/navidrome/server/subsonic"
"sync"
)
// Injectors from wire_injectors.go:
@@ -31,16 +31,17 @@ 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)
playlists := core.NewPlaylists(dataStore)
router := nativeapi.New(dataStore, share, playlists)
return router
}
@@ -54,14 +55,16 @@ 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)
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)
archiver := core.NewArchiver(mediaStreamer, dataStore, share)
players := core.NewPlayers(dataStore)
playlists := core.NewPlaylists(dataStore)
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
broker := events.GetBroker()
scannerScanner := scanner.GetInstance(dataStore, playlists, cacheWarmer, broker)
playTracker := scrobbler.GetPlayTracker(dataStore, broker)
playbackServer := playback.GetInstance(dataStore)
router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, externalMetadata, scannerScanner, broker, playlists, playTracker, share, playbackServer)
return router
}
@@ -76,7 +79,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
}
@@ -94,7 +98,7 @@ func CreateListenBrainzRouter() *listenbrainz.Router {
return router
}
func createScanner() scanner.Scanner {
func GetScanner() scanner.Scanner {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
playlists := core.NewPlaylists(dataStore)
@@ -105,23 +109,17 @@ func createScanner() scanner.Scanner {
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata)
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
broker := events.GetBroker()
scannerScanner := scanner.New(dataStore, playlists, cacheWarmer, broker)
scannerScanner := scanner.GetInstance(dataStore, playlists, cacheWarmer, broker)
return scannerScanner
}
func GetPlaybackServer() playback.PlaybackServer {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
playbackServer := playback.GetInstance(dataStore)
return playbackServer
}
// wire_injectors.go:
var allProviders = wire.NewSet(core.Set, artwork.Set, subsonic.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, db.Db)
// Scanner must be a Singleton
var (
onceScanner sync.Once
scannerInstance scanner.Scanner
)
func GetScanner() scanner.Scanner {
onceScanner.Do(func() {
scannerInstance = createScanner()
})
return scannerInstance
}
var allProviders = wire.NewSet(core.Set, artwork.Set, server.New, subsonic.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, scanner.GetInstance, db.Db)

View File

@@ -3,13 +3,12 @@
package cmd
import (
"sync"
"github.com/google/wire"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/agents/lastfm"
"github.com/navidrome/navidrome/core/agents/listenbrainz"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/playback"
"github.com/navidrome/navidrome/db"
"github.com/navidrome/navidrome/persistence"
"github.com/navidrome/navidrome/scanner"
@@ -23,6 +22,7 @@ import (
var allProviders = wire.NewSet(
core.Set,
artwork.Set,
server.New,
subsonic.New,
nativeapi.New,
public.New,
@@ -30,12 +30,12 @@ var allProviders = wire.NewSet(
lastfm.NewRouter,
listenbrainz.NewRouter,
events.GetBroker,
scanner.GetInstance,
db.Db,
)
func CreateServer(musicFolder string) *server.Server {
panic(wire.Build(
server.New,
allProviders,
))
}
@@ -49,7 +49,6 @@ func CreateNativeAPIRouter() *nativeapi.Router {
func CreateSubsonicAPIRouter() *subsonic.Router {
panic(wire.Build(
allProviders,
GetScanner,
))
}
@@ -71,22 +70,14 @@ func CreateListenBrainzRouter() *listenbrainz.Router {
))
}
// Scanner must be a Singleton
var (
onceScanner sync.Once
scannerInstance scanner.Scanner
)
func GetScanner() scanner.Scanner {
onceScanner.Do(func() {
scannerInstance = createScanner()
})
return scannerInstance
}
func createScanner() scanner.Scanner {
panic(wire.Build(
allProviders,
scanner.New,
))
}
func GetPlaybackServer() playback.PlaybackServer {
panic(wire.Build(
allProviders,
))
}

View File

@@ -2,6 +2,7 @@ package conf
import (
"fmt"
"net/url"
"os"
"path/filepath"
"runtime"
@@ -11,7 +12,6 @@ import (
"github.com/kr/pretty"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/utils/number"
"github.com/robfig/cron/v3"
"github.com/spf13/viper"
)
@@ -20,14 +20,21 @@ type configOptions struct {
ConfigFile string
Address string
Port int
UnixSocketPerm string
MusicFolder string
DataFolder string
CacheFolder string
DbPath string
LogLevel string
ScanInterval time.Duration
ScanSchedule string
SessionTimeout time.Duration
BaseURL string
BasePath string
BaseHost string
BaseScheme string
TLSCert string
TLSKey string
UILoginBackgroundURL string
UIWelcomeMessage string
MaxSidebarPlaylists int
@@ -37,6 +44,7 @@ type configOptions struct {
EnableMediaFileCoverArt bool
TranscodingCacheSize string
ImageCacheSize string
AlbumPlayCountMode string
EnableArtworkPrecache bool
AutoImportPlaylists bool
PlaylistsPath string
@@ -44,17 +52,22 @@ type configOptions struct {
DefaultDownsamplingFormat string
SearchFullString bool
RecentlyAddedByModTime bool
PreferSortTags bool
IgnoredArticles string
IndexGroups string
SubsonicArtistParticipations bool
FFmpegPath string
MPVPath string
MPVCmdTemplate 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
@@ -67,8 +80,10 @@ type configOptions struct {
PasswordEncryptionKey string
ReverseProxyUserHeader string
ReverseProxyWhitelist string
HTTPSecurityHeaders secureOptions
Prometheus prometheusOptions
Scanner scannerOptions
Jukebox jukeboxOptions
Agents string
LastFM lastfmOptions
@@ -78,12 +93,14 @@ 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
DevSidebarPlaylists bool
DevEnableBufferedScrobble bool
DevShowArtistPage bool
DevOffsetOptimize int
DevArtworkMaxRequests int
DevArtworkThrottleBacklogLimit int
DevArtworkThrottleBacklogTimeout time.Duration
@@ -92,8 +109,9 @@ type configOptions struct {
}
type scannerOptions struct {
Extractor string
GenreSeparators string
Extractor string
GenreSeparators string
GroupAlbumReleases bool
}
type lastfmOptions struct {
@@ -113,11 +131,24 @@ type listenBrainzOptions struct {
BaseURL string
}
type secureOptions struct {
CustomFrameOptionsValue string
}
type prometheusOptions struct {
Enabled bool
MetricsPath string
}
type AudioDeviceDefinition []string
type jukeboxOptions struct {
Enabled bool
Devices []AudioDeviceDefinition
Default string
AdminOnly bool
}
var (
Server = &configOptions{}
hooks []func()
@@ -125,6 +156,11 @@ var (
func LoadFromFile(confFile string) {
viper.SetConfigFile(confFile)
err := viper.ReadInConfig()
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "FATAL: Error reading config file:", err)
os.Exit(1)
}
Load()
}
@@ -139,6 +175,16 @@ func Load() {
_, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating data path:", "path", Server.DataFolder, err)
os.Exit(1)
}
if Server.CacheFolder == "" {
Server.CacheFolder = filepath.Join(Server.DataFolder, "cache")
}
err = os.MkdirAll(Server.CacheFolder, os.ModePerm)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating cache path:", "path", Server.CacheFolder, err)
os.Exit(1)
}
Server.ConfigFile = viper.GetViper().ConfigFileUsed()
if Server.DbPath == "" {
Server.DbPath = filepath.Join(Server.DataFolder, consts.DefaultDbPath)
@@ -153,8 +199,21 @@ 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 {
if log.IsGreaterOrEqualTo(log.LevelDebug) {
prettyConf := pretty.Sprintf("Loaded configuration from '%s': %# v", Server.ConfigFile, Server)
if Server.EnableLogRedacting {
prettyConf = log.Redact(prettyConf)
@@ -219,36 +278,45 @@ func AddHook(hook func()) {
func init() {
viper.SetDefault("musicfolder", filepath.Join(".", "music"))
viper.SetDefault("cachefolder", "")
viper.SetDefault("datafolder", ".")
viper.SetDefault("loglevel", "info")
viper.SetDefault("address", "0.0.0.0")
viper.SetDefault("port", 4533)
viper.SetDefault("unixsocketperm", "0660")
viper.SetDefault("sessiontimeout", consts.DefaultSessionTimeout)
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("albumplaycountmode", consts.AlbumPlayCountModeAbsolute)
viper.SetDefault("enableartworkprecache", true)
viper.SetDefault("autoimportplaylists", true)
viper.SetDefault("playlistspath", consts.DefaultPlaylistsPath)
viper.SetDefault("enabledownloads", true)
viper.SetDefault("enableexternalservices", true)
viper.SetDefault("enableMediaFileCoverArt", true)
viper.SetDefault("enablemediafilecoverart", true)
viper.SetDefault("autotranscodedownload", false)
viper.SetDefault("defaultdownsamplingformat", consts.DefaultDownsamplingFormat)
viper.SetDefault("searchfullstring", false)
viper.SetDefault("recentlyaddedbymodtime", false)
viper.SetDefault("prefersorttags", false)
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("ffmpegpath", "")
viper.SetDefault("mpvcmdtemplate", "mpv --audio-device=%d --no-audio-display --pause %f --input-ipc-server=%s")
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)
@@ -270,29 +338,40 @@ func init() {
viper.SetDefault("prometheus.enabled", false)
viper.SetDefault("prometheus.metricspath", "/metrics")
viper.SetDefault("jukebox.enabled", false)
viper.SetDefault("jukebox.devices", []AudioDeviceDefinition{})
viper.SetDefault("jukebox.default", "")
viper.SetDefault("jukebox.adminonly", true)
viper.SetDefault("scanner.extractor", consts.DefaultScannerExtractor)
viper.SetDefault("scanner.genreseparators", ";/,")
viper.SetDefault("scanner.groupalbumreleases", false)
viper.SetDefault("agents", "lastfm,spotify")
viper.SetDefault("lastfm.enabled", true)
viper.SetDefault("lastfm.language", "en")
viper.SetDefault("lastfm.apikey", consts.LastFMAPIKey)
viper.SetDefault("lastfm.secret", consts.LastFMAPISecret)
viper.SetDefault("lastfm.apikey", "")
viper.SetDefault("lastfm.secret", "")
viper.SetDefault("spotify.id", "")
viper.SetDefault("spotify.secret", "")
viper.SetDefault("listenbrainz.enabled", true)
viper.SetDefault("listenbrainz.baseurl", "https://api.listenbrainz.org/1/")
viper.SetDefault("httpsecurityheaders.customframeoptionsvalue", "DENY")
// 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)
viper.SetDefault("devartworkmaxrequests", number.Max(2, runtime.NumCPU()/3))
viper.SetDefault("devoffsetoptimize", 50000)
viper.SetDefault("devartworkmaxrequests", max(2, runtime.NumCPU()/3))
viper.SetDefault("devartworkthrottlebackloglimit", consts.RequestThrottleBacklogLimit)
viper.SetDefault("devartworkthrottlebacklogtimeout", consts.RequestThrottleBacklogTimeout)
viper.SetDefault("devartistinfotimetolive", consts.ArtistInfoTimeToLive)

47
conf/mime/mime_types.go Normal file
View File

@@ -0,0 +1,47 @@
package mime
import (
"mime"
"strings"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/resources"
"gopkg.in/yaml.v3"
)
type mimeConf struct {
Types map[string]string `yaml:"types"`
Lossless []string `yaml:"lossless"`
}
var LosslessFormats []string
func initMimeTypes() {
// In some circumstances, Windows sets JS mime-type to `text/plain`!
_ = mime.AddExtensionType(".js", "text/javascript")
_ = mime.AddExtensionType(".css", "text/css")
f, err := resources.FS().Open("mime_types.yaml")
if err != nil {
log.Fatal("Fatal error opening mime_types.yaml", err)
}
defer f.Close()
var mimeConf mimeConf
err = yaml.NewDecoder(f).Decode(&mimeConf)
if err != nil {
log.Fatal("Fatal error parsing mime_types.yaml", err)
}
for ext, typ := range mimeConf.Types {
_ = mime.AddExtensionType(ext, typ)
}
for _, ext := range mimeConf.Lossless {
LosslessFormats = append(LosslessFormats, strings.TrimPrefix(ext, "."))
}
}
func init() {
conf.AddHook(initMimeTypes)
}

View File

@@ -71,42 +71,46 @@ const (
// Cache options
const (
TranscodingCacheDir = "cache/transcoding"
TranscodingCacheDir = "transcoding"
DefaultTranscodingCacheMaxItems = 0 // Unlimited
ImageCacheDir = "cache/images"
ImageCacheDir = "images"
DefaultImageCacheMaxItems = 0 // Unlimited
DefaultCacheSize = 100 * 1024 * 1024 // 100MB
DefaultCacheCleanUpInterval = 10 * time.Minute
)
// Shared secrets (only add here "secrets" that can be public)
const (
LastFMAPIKey = "9b94a5515ea66b2da3ec03c12300327e" // nolint:gosec
LastFMAPISecret = "74cb6557cec7171d921af5d7d887c587" // nolint:gosec
AlbumPlayCountModeAbsolute = "absolute"
AlbumPlayCountModeNormalized = "normalized"
)
var (
DefaultDownsamplingFormat = "opus"
DefaultTranscodings = []map[string]interface{}{
DefaultTranscodings = []struct {
Name string
TargetFormat string
DefaultBitRate int
Command string
}{
{
"name": "mp3 audio",
"targetFormat": "mp3",
"defaultBitRate": 192,
"command": "ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -f mp3 -",
Name: "mp3 audio",
TargetFormat: "mp3",
DefaultBitRate: 192,
Command: "ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -f mp3 -",
},
{
"name": "opus audio",
"targetFormat": "opus",
"defaultBitRate": 128,
"command": "ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -c:a libopus -f opus -",
Name: "opus audio",
TargetFormat: "opus",
DefaultBitRate: 128,
Command: "ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -c:a libopus -f opus -",
},
{
"name": "aac audio",
"targetFormat": "aac",
"defaultBitRate": 256,
"command": "ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -c:a aac -f adts -",
Name: "aac audio",
TargetFormat: "aac",
DefaultBitRate: 256,
Command: "ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -c:a aac -f adts -",
},
}

View File

@@ -1,64 +0,0 @@
package consts
import (
"mime"
"sort"
"strings"
)
type format struct {
typ string
lossless bool
}
var audioFormats = map[string]format{
".mp3": {typ: "audio/mpeg"},
".ogg": {typ: "audio/ogg"},
".oga": {typ: "audio/ogg"},
".opus": {typ: "audio/ogg"},
".aac": {typ: "audio/mp4"},
".alac": {typ: "audio/mp4", lossless: true},
".m4a": {typ: "audio/mp4"},
".m4b": {typ: "audio/mp4"},
".flac": {typ: "audio/flac", lossless: true},
".wav": {typ: "audio/x-wav", lossless: true},
".wma": {typ: "audio/x-ms-wma"},
".ape": {typ: "audio/x-monkeys-audio", lossless: true},
".mpc": {typ: "audio/x-musepack"},
".shn": {typ: "audio/x-shn", lossless: true},
".aif": {typ: "audio/x-aiff"},
".aiff": {typ: "audio/x-aiff"},
".m3u": {typ: "audio/x-mpegurl"},
".pls": {typ: "audio/x-scpls"},
".dsf": {typ: "audio/dsd", lossless: true},
".wv": {typ: "audio/x-wavpack", lossless: true},
".wvp": {typ: "audio/x-wavpack", lossless: true},
".mka": {typ: "audio/x-matroska"},
}
var imageFormats = map[string]string{
".gif": "image/gif",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".webp": "image/webp",
".png": "image/png",
".bmp": "image/bmp",
}
var LosslessFormats []string
func init() {
for ext, fmt := range audioFormats {
_ = mime.AddExtensionType(ext, fmt.typ)
if fmt.lossless {
LosslessFormats = append(LosslessFormats, strings.TrimPrefix(ext, "."))
}
}
sort.Strings(LosslessFormats)
for ext, typ := range imageFormats {
_ = mime.AddExtensionType(ext, typ)
}
// In some circumstances, Windows sets JS mime-type to `text/plain`!
_ = mime.AddExtensionType(".js", "text/javascript")
_ = mime.AddExtensionType(".css", "text/css")
}

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"

View File

@@ -11,7 +11,7 @@
#
# navidrome_enable (bool): Set to YES to enable navidrome
# Default: NO
# navidrome_config (str): navidrome configration file
# navidrome_config (str): navidrome configuration file
# Default: /usr/local/etc/navidrome/config.toml
# navidrome_datafolder (str): navidrome Folder to store application data
# Default: www

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

@@ -134,7 +134,7 @@ func (a *Agents) GetSimilarArtists(ctx context.Context, id, name, mbid string, l
}
similar, err := agent.GetSimilarArtists(ctx, id, name, mbid, limit)
if len(similar) > 0 && err == nil {
if log.CurrentLevel() >= log.LevelTrace {
if log.IsGreaterOrEqualTo(log.LevelTrace) {
log.Debug(ctx, "Got Similar Artists", "agent", ag.AgentName(), "artist", name, "similar", similar, "elapsed", time.Since(start))
} else {
log.Debug(ctx, "Got Similar Artists", "agent", ag.AgentName(), "artist", name, "similarReceived", len(similar), "elapsed", time.Since(start))

View File

@@ -14,7 +14,7 @@ import (
"github.com/navidrome/navidrome/core/scrobbler"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/cache"
)
const (
@@ -47,7 +47,7 @@ func lastFMConstructor(ds model.DataStore) *lastfmAgent {
hc := &http.Client{
Timeout: consts.DefaultHttpClientTimeOut,
}
chc := utils.NewCachedHTTPClient(hc, consts.DefaultHttpClientTimeOut)
chc := cache.NewHTTPClient(hc, consts.DefaultHttpClientTimeOut)
l.client = newClient(l.apiKey, l.secret, l.lang, chc)
return l
}
@@ -257,7 +257,7 @@ func (l *lastfmAgent) NowPlaying(ctx context.Context, userId string, track *mode
track: track.Title,
album: track.Album,
trackNumber: track.TrackNumber,
mbid: track.MbzTrackID,
mbid: track.MbzRecordingID,
duration: int(track.Duration),
albumArtist: track.AlbumArtist,
})
@@ -283,7 +283,7 @@ func (l *lastfmAgent) Scrobble(ctx context.Context, userId string, s scrobbler.S
track: s.Title,
album: s.Album,
trackNumber: s.TrackNumber,
mbid: s.MbzTrackID,
mbid: s.MbzRecordingID,
duration: int(s.Duration),
albumArtist: s.AlbumArtist,
timestamp: s.TimeStamp,
@@ -311,12 +311,14 @@ func (l *lastfmAgent) IsAuthorized(ctx context.Context, userId string) bool {
func init() {
conf.AddHook(func() {
if conf.Server.LastFM.Enabled {
agents.Register(lastFMAgentName, func(ds model.DataStore) agents.Interface {
return lastFMConstructor(ds)
})
scrobbler.Register(lastFMAgentName, func(ds model.DataStore) scrobbler.Scrobbler {
return lastFMConstructor(ds)
})
if conf.Server.LastFM.ApiKey != "" && conf.Server.LastFM.Secret != "" {
agents.Register(lastFMAgentName, func(ds model.DataStore) agents.Interface {
return lastFMConstructor(ds)
})
scrobbler.Register(lastFMAgentName, func(ds model.DataStore) scrobbler.Scrobbler {
return lastFMConstructor(ds)
})
}
}
})
}

View File

@@ -234,14 +234,14 @@ var _ = Describe("lastfmAgent", func() {
agent = lastFMConstructor(ds)
agent.client = client
track = &model.MediaFile{
ID: "123",
Title: "Track Title",
Album: "Track Album",
Artist: "Track Artist",
AlbumArtist: "Track AlbumArtist",
TrackNumber: 1,
Duration: 180,
MbzTrackID: "mbz-123",
ID: "123",
Title: "Track Title",
Album: "Track Album",
Artist: "Track Artist",
AlbumArtist: "Track AlbumArtist",
TrackNumber: 1,
Duration: 180,
MbzRecordingID: "mbz-123",
}
})
@@ -262,7 +262,7 @@ var _ = Describe("lastfmAgent", func() {
Expect(sentParams.Get("albumArtist")).To(Equal(track.AlbumArtist))
Expect(sentParams.Get("trackNumber")).To(Equal(strconv.Itoa(track.TrackNumber)))
Expect(sentParams.Get("duration")).To(Equal(strconv.FormatFloat(float64(track.Duration), 'G', -1, 32)))
Expect(sentParams.Get("mbid")).To(Equal(track.MbzTrackID))
Expect(sentParams.Get("mbid")).To(Equal(track.MbzRecordingID))
})
It("returns ErrNotAuthorized if user is not linked", func() {
@@ -289,7 +289,7 @@ var _ = Describe("lastfmAgent", func() {
Expect(sentParams.Get("albumArtist")).To(Equal(track.AlbumArtist))
Expect(sentParams.Get("trackNumber")).To(Equal(strconv.Itoa(track.TrackNumber)))
Expect(sentParams.Get("duration")).To(Equal(strconv.FormatFloat(float64(track.Duration), 'G', -1, 32)))
Expect(sentParams.Get("mbid")).To(Equal(track.MbzTrackID))
Expect(sentParams.Get("mbid")).To(Equal(track.MbzRecordingID))
Expect(sentParams.Get("timestamp")).To(Equal(strconv.FormatInt(ts.Unix(), 10)))
})

View File

@@ -18,7 +18,7 @@ import (
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/server"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/req"
)
//go:embed token_received.html
@@ -89,13 +89,14 @@ func (s *Router) unlink(w http.ResponseWriter, r *http.Request) {
}
func (s *Router) callback(w http.ResponseWriter, r *http.Request) {
token := utils.ParamString(r, "token")
if token == "" {
p := req.Params(r)
token, err := p.String("token")
if err != nil {
_ = rest.RespondWithError(w, http.StatusBadRequest, "token not received")
return
}
uid := utils.ParamString(r, "uid")
if uid == "" {
uid, err := p.String("uid")
if err != nil {
_ = rest.RespondWithError(w, http.StatusBadRequest, "uid not received")
return
}
@@ -103,7 +104,7 @@ func (s *Router) callback(w http.ResponseWriter, r *http.Request) {
// Need to add user to context, as this is a non-authenticated endpoint, so it does not
// automatically contain any user info
ctx := request.WithUser(r.Context(), model.User{ID: uid})
err := s.fetchSessionKey(ctx, uid, token)
err = s.fetchSessionKey(ctx, uid, token)
if err != nil {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusBadRequest)

View File

@@ -8,13 +8,13 @@ import (
"fmt"
"net/http"
"net/url"
"slices"
"sort"
"strconv"
"strings"
"time"
"github.com/navidrome/navidrome/log"
"golang.org/x/exp/slices"
)
const (

View File

@@ -11,7 +11,7 @@ import (
"github.com/navidrome/navidrome/core/scrobbler"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/cache"
)
const (
@@ -35,7 +35,7 @@ func listenBrainzConstructor(ds model.DataStore) *listenBrainzAgent {
hc := &http.Client{
Timeout: consts.DefaultHttpClientTimeOut,
}
chc := utils.NewCachedHTTPClient(hc, consts.DefaultHttpClientTimeOut)
chc := cache.NewHTTPClient(hc, consts.DefaultHttpClientTimeOut)
l.client = newClient(l.baseURL, chc)
return l
}
@@ -55,8 +55,9 @@ func (l *listenBrainzAgent) formatListen(track *model.MediaFile) listenInfo {
SubmissionClientVersion: consts.Version,
TrackNumber: track.TrackNumber,
ArtistMbzIDs: []string{track.MbzArtistID},
TrackMbzID: track.MbzTrackID,
RecordingMbzID: track.MbzRecordingID,
ReleaseMbID: track.MbzAlbumID,
DurationMs: int(track.Duration * 1000),
},
},
}

View File

@@ -32,14 +32,15 @@ var _ = Describe("listenBrainzAgent", func() {
agent = listenBrainzConstructor(ds)
agent.client = newClient("http://localhost:8080", httpClient)
track = &model.MediaFile{
ID: "123",
Title: "Track Title",
Album: "Track Album",
Artist: "Track Artist",
TrackNumber: 1,
MbzTrackID: "mbz-123",
MbzAlbumID: "mbz-456",
MbzArtistID: "mbz-789",
ID: "123",
Title: "Track Title",
Album: "Track Album",
Artist: "Track Artist",
TrackNumber: 1,
MbzRecordingID: "mbz-123",
MbzAlbumID: "mbz-456",
MbzArtistID: "mbz-789",
Duration: 142.2,
}
})
@@ -60,11 +61,12 @@ var _ = Describe("listenBrainzAgent", func() {
"SubmissionClient": Equal(consts.AppName),
"SubmissionClientVersion": Equal(consts.Version),
"TrackNumber": Equal(track.TrackNumber),
"TrackMbzID": Equal(track.MbzTrackID),
"RecordingMbzID": Equal(track.MbzRecordingID),
"ReleaseMbID": Equal(track.MbzAlbumID),
"ArtistMbzIDs": MatchAllElements(idArtistId, Elements{
"mbz-789": Equal(track.MbzArtistID),
}),
"DurationMs": Equal(142200),
}),
}),
}))

View File

@@ -76,9 +76,10 @@ type additionalInfo struct {
SubmissionClient string `json:"submission_client,omitempty"`
SubmissionClientVersion string `json:"submission_client_version,omitempty"`
TrackNumber int `json:"tracknumber,omitempty"`
TrackMbzID string `json:"track_mbid,omitempty"`
RecordingMbzID string `json:"recording_mbid,omitempty"`
ArtistMbzIDs []string `json:"artist_mbids,omitempty"`
ReleaseMbID string `json:"release_mbid,omitempty"`
DurationMs int `json:"duration_ms,omitempty"`
}
func (c *client) validateToken(ctx context.Context, apiKey string) (*listenBrainzResponse, error) {

View File

@@ -74,10 +74,11 @@ var _ = Describe("client", func() {
TrackName: "Track Title",
ReleaseName: "Track Album",
AdditionalInfo: additionalInfo{
TrackNumber: 1,
TrackMbzID: "mbz-123",
ArtistMbzIDs: []string{"mbz-789"},
ReleaseMbID: "mbz-456",
TrackNumber: 1,
RecordingMbzID: "mbz-123",
ArtistMbzIDs: []string{"mbz-789"},
ReleaseMbID: "mbz-456",
DurationMs: 142200,
},
},
}

View File

@@ -13,7 +13,7 @@ import (
"github.com/navidrome/navidrome/core/agents"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/cache"
"github.com/xrash/smetrics"
)
@@ -35,7 +35,7 @@ func spotifyConstructor(ds model.DataStore) agents.Interface {
hc := &http.Client{
Timeout: consts.DefaultHttpClientTimeOut,
}
chc := utils.NewCachedHTTPClient(hc, consts.DefaultHttpClientTimeOut)
chc := cache.NewHTTPClient(hc, consts.DefaultHttpClientTimeOut)
l.client = newClient(l.id, l.secret, chc)
return l
}

View File

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

211
core/archiver_test.go Normal file
View File

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

View File

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

View File

@@ -131,7 +131,7 @@ func (a *cacheWarmer) doCacheImage(ctx context.Context, id model.ArtworkID) erro
r, _, err := a.artwork.Get(ctx, id, consts.UICoverArtSize)
if err != nil {
return fmt.Errorf("error cacheing id='%s': %w", id, err)
return fmt.Errorf("error caching id='%s': %w", id, err)
}
defer r.Close()
_, err = io.Copy(io.Discard, r)

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

@@ -16,7 +16,6 @@ import (
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/number"
)
type resizedArtworkReader struct {
@@ -113,7 +112,7 @@ func resizeImage(reader io.Reader, size int) (io.Reader, int, error) {
// Don't upscale the image
bounds := img.Bounds()
originalSize := number.Max(bounds.Max.X, bounds.Max.Y)
originalSize := max(bounds.Max.X, bounds.Max.Y)
if originalSize <= size {
return nil, originalSize, nil
}

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/go-chi/jwtauth/v5"
"github.com/google/uuid"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
@@ -23,9 +24,10 @@ var (
func Init(ds model.DataStore) {
once.Do(func() {
log.Info("Setting Session Timeout", "value", conf.Server.SessionTimeout)
secret, err := ds.Property(context.TODO()).DefaultGet(consts.JWTSecretKey, "not so secret")
if err != nil {
secret, err := ds.Property(context.TODO()).Get(consts.JWTSecretKey)
if err != nil || secret == "" {
log.Error("No JWT secret found in DB. Setting a temp one, but please report this error", err)
secret = uuid.NewString()
}
Secret = []byte(secret)
TokenAuth = jwtauth.New("HS256", Secret, nil)

View File

@@ -9,7 +9,6 @@ import (
"time"
"github.com/Masterminds/squirrel"
"github.com/deluan/sanitize"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/core/agents"
_ "github.com/navidrome/navidrome/core/agents/lastfm"
@@ -18,7 +17,8 @@ import (
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/number"
. "github.com/navidrome/navidrome/utils/gg"
"github.com/navidrome/navidrome/utils/random"
"golang.org/x/sync/errgroup"
)
@@ -90,15 +90,16 @@ func (e *externalMetadata) UpdateAlbumInfo(ctx context.Context, id string) (*mod
return nil, err
}
if album.ExternalInfoUpdatedAt.IsZero() {
log.Debug(ctx, "AlbumInfo not cached. Retrieving it now", "updatedAt", album.ExternalInfoUpdatedAt, "id", id, "name", album.Name)
updatedAt := V(album.ExternalInfoUpdatedAt)
if updatedAt.IsZero() {
log.Debug(ctx, "AlbumInfo not cached. Retrieving it now", "updatedAt", updatedAt, "id", id, "name", album.Name)
err = e.populateAlbumInfo(ctx, album)
if err != nil {
return nil, err
}
}
if time.Since(album.ExternalInfoUpdatedAt) > conf.Server.DevAlbumInfoTimeToLive {
if time.Since(updatedAt) > conf.Server.DevAlbumInfoTimeToLive {
log.Debug("Found expired cached AlbumInfo, refreshing in the background", "updatedAt", album.ExternalInfoUpdatedAt, "name", album.Name)
enqueueRefresh(e.albumQueue, album)
}
@@ -118,7 +119,7 @@ func (e *externalMetadata) populateAlbumInfo(ctx context.Context, album *auxAlbu
return err
}
album.ExternalInfoUpdatedAt = time.Now()
album.ExternalInfoUpdatedAt = P(time.Now())
album.ExternalUrl = info.URL
if info.Description != "" {
@@ -202,8 +203,9 @@ func (e *externalMetadata) refreshArtistInfo(ctx context.Context, id string) (*a
}
// If we don't have any info, retrieves it now
if artist.ExternalInfoUpdatedAt.IsZero() {
log.Debug(ctx, "ArtistInfo not cached. Retrieving it now", "updatedAt", artist.ExternalInfoUpdatedAt, "id", id, "name", artist.Name)
updatedAt := V(artist.ExternalInfoUpdatedAt)
if updatedAt.IsZero() {
log.Debug(ctx, "ArtistInfo not cached. Retrieving it now", "updatedAt", updatedAt, "id", id, "name", artist.Name)
err := e.populateArtistInfo(ctx, artist)
if err != nil {
return nil, err
@@ -211,8 +213,8 @@ func (e *externalMetadata) refreshArtistInfo(ctx context.Context, id string) (*a
}
// If info is expired, trigger a populateArtistInfo in the background
if time.Since(artist.ExternalInfoUpdatedAt) > conf.Server.DevArtistInfoTimeToLive {
log.Debug("Found expired cached ArtistInfo, refreshing in the background", "updatedAt", artist.ExternalInfoUpdatedAt, "name", artist.Name)
if time.Since(updatedAt) > conf.Server.DevArtistInfoTimeToLive {
log.Debug("Found expired cached ArtistInfo, refreshing in the background", "updatedAt", updatedAt, "name", artist.Name)
enqueueRefresh(e.artistQueue, artist)
}
return artist, nil
@@ -242,7 +244,7 @@ func (e *externalMetadata) populateArtistInfo(ctx context.Context, artist *auxAr
return ctx.Err()
}
artist.ExternalInfoUpdatedAt = time.Now()
artist.ExternalInfoUpdatedAt = P(time.Now())
err := e.ds.Artist(ctx).Put(&artist.Artist)
if err != nil {
log.Error(ctx, "Error trying to update artist external information", "id", artist.ID, "name", artist.Name,
@@ -265,14 +267,14 @@ func (e *externalMetadata) SimilarSongs(ctx context.Context, id string, count in
return nil, ctx.Err()
}
weightedSongs := utils.NewWeightedRandomChooser()
addArtist := func(a model.Artist, weightedSongs *utils.WeightedChooser, count, artistWeight int) error {
weightedSongs := random.NewWeightedRandomChooser()
addArtist := func(a model.Artist, weightedSongs *random.WeightedChooser, count, artistWeight int) error {
if utils.IsCtxDone(ctx) {
log.Warn(ctx, "SimilarSongs call canceled", ctx.Err())
return ctx.Err()
}
topCount := number.Max(count, 20)
topCount := max(count, 20)
topSongs, err := e.getMatchingTopSongs(ctx, e.ag, &auxArtist{Name: a.Name, Artist: a}, topCount)
if err != nil {
log.Warn(ctx, "Error getting artist's top songs", "artist", a.Name, err)
@@ -399,7 +401,7 @@ func (e *externalMetadata) getMatchingTopSongs(ctx context.Context, agent agents
func (e *externalMetadata) findMatchingTrack(ctx context.Context, mbid string, artistID, title string) (*model.MediaFile, error) {
if mbid != "" {
mfs, err := e.ds.MediaFile(ctx).GetAll(model.QueryOptions{
Filters: squirrel.Eq{"mbz_track_id": mbid},
Filters: squirrel.Eq{"mbz_recording_id": mbid},
})
if err == nil && len(mfs) > 0 {
return &mfs[0], nil
@@ -412,9 +414,9 @@ func (e *externalMetadata) findMatchingTrack(ctx context.Context, mbid string, a
squirrel.Eq{"artist_id": artistID},
squirrel.Eq{"album_artist_id": artistID},
},
squirrel.Like{"order_title": strings.TrimSpace(sanitize.Accents(title))},
squirrel.Like{"order_title": utils.SanitizeFieldForSorting(title)},
},
Sort: "starred desc, rating desc, year asc",
Sort: "starred desc, rating desc, year asc, compilation asc ",
Max: 1,
})
if err != nil || len(mfs) == 0 {

View File

@@ -16,10 +16,14 @@ import (
)
type FFmpeg interface {
Transcode(ctx context.Context, command, path string, maxBitRate int) (io.ReadCloser, error)
Transcode(ctx context.Context, command, path string, maxBitRate, offset int) (io.ReadCloser, error)
ExtractImage(ctx context.Context, path string) (io.ReadCloser, error)
ConvertToWAV(ctx context.Context, path string) (io.ReadCloser, error)
ConvertToFLAC(ctx context.Context, path string) (io.ReadCloser, error)
Probe(ctx context.Context, files []string) (string, error)
CmdPath() (string, error)
IsAvailable() bool
Version() string
}
func New() FFmpeg {
@@ -29,15 +33,17 @@ func New() FFmpeg {
const (
extractImageCmd = "ffmpeg -i %s -an -vcodec copy -f image2pipe -"
probeCmd = "ffmpeg %s -f ffmetadata"
createWavCmd = "ffmpeg -i %s -c:a pcm_s16le -f wav -"
createFLACCmd = "ffmpeg -i %s -f flac -"
)
type ffmpeg struct{}
func (e *ffmpeg) Transcode(ctx context.Context, command, path string, maxBitRate int) (io.ReadCloser, error) {
func (e *ffmpeg) Transcode(ctx context.Context, command, path string, maxBitRate, offset int) (io.ReadCloser, error) {
if _, err := ffmpegCmd(); err != nil {
return nil, err
}
args := createFFmpegCommand(command, path, maxBitRate)
args := createFFmpegCommand(command, path, maxBitRate, offset)
return e.start(ctx, args)
}
@@ -45,7 +51,17 @@ func (e *ffmpeg) ExtractImage(ctx context.Context, path string) (io.ReadCloser,
if _, err := ffmpegCmd(); err != nil {
return nil, err
}
args := createFFmpegCommand(extractImageCmd, path, 0)
args := createFFmpegCommand(extractImageCmd, path, 0, 0)
return e.start(ctx, args)
}
func (e *ffmpeg) ConvertToWAV(ctx context.Context, path string) (io.ReadCloser, error) {
args := createFFmpegCommand(createWavCmd, path, 0, 0)
return e.start(ctx, args)
}
func (e *ffmpeg) ConvertToFLAC(ctx context.Context, path string) (io.ReadCloser, error) {
args := createFFmpegCommand(createFLACCmd, path, 0, 0)
return e.start(ctx, args)
}
@@ -64,6 +80,29 @@ func (e *ffmpeg) CmdPath() (string, error) {
return ffmpegCmd()
}
func (e *ffmpeg) IsAvailable() bool {
_, err := ffmpegCmd()
return err == nil
}
// Version executes ffmpeg -version and extracts the version from the output.
// Sample output: ffmpeg version 6.0 Copyright (c) 2000-2023 the FFmpeg developers
func (e *ffmpeg) Version() string {
cmd, err := ffmpegCmd()
if err != nil {
return "N/A"
}
out, err := exec.Command(cmd, "-version").CombinedOutput() // #nosec
if err != nil {
return "N/A"
}
parts := strings.Split(string(out), " ")
if len(parts) < 3 {
return "N/A"
}
return parts[2]
}
func (e *ffmpeg) start(ctx context.Context, args []string) (io.ReadCloser, error) {
log.Trace(ctx, "Executing ffmpeg command", "cmd", args)
j := &ffCmd{args: args}
@@ -86,7 +125,7 @@ type ffCmd struct {
func (j *ffCmd) start() error {
cmd := exec.Command(j.args[0], j.args[1:]...) // #nosec
cmd.Stdout = j.out
if log.CurrentLevel() >= log.LevelTrace {
if log.IsGreaterOrEqualTo(log.LevelTrace) {
cmd.Stderr = os.Stderr
} else {
cmd.Stderr = io.Discard
@@ -113,15 +152,25 @@ func (j *ffCmd) wait() {
}
// Path will always be an absolute path
func createFFmpegCommand(cmd, path string, maxBitRate int) []string {
func createFFmpegCommand(cmd, path string, maxBitRate, offset int) []string {
split := strings.Split(fixCmd(cmd), " ")
for i, s := range split {
s = strings.ReplaceAll(s, "%s", path)
s = strings.ReplaceAll(s, "%b", strconv.Itoa(maxBitRate))
split[i] = s
var parts []string
for _, s := range split {
if strings.Contains(s, "%s") {
s = strings.ReplaceAll(s, "%s", path)
parts = append(parts, s)
if offset > 0 && !strings.Contains(cmd, "%t") {
parts = append(parts, "-ss", strconv.Itoa(offset))
}
} else {
s = strings.ReplaceAll(s, "%t", strconv.Itoa(offset))
s = strings.ReplaceAll(s, "%b", strconv.Itoa(maxBitRate))
parts = append(parts, s)
}
}
return split
return parts
}
func createProbeCommand(cmd string, inputs []string) []string {

View File

@@ -24,9 +24,22 @@ var _ = Describe("ffmpeg", func() {
})
Describe("createFFmpegCommand", func() {
It("creates a valid command line", func() {
args := createFFmpegCommand("ffmpeg -i %s -b:a %bk mp3 -", "/music library/file.mp3", 123)
args := createFFmpegCommand("ffmpeg -i %s -b:a %bk mp3 -", "/music library/file.mp3", 123, 0)
Expect(args).To(Equal([]string{"ffmpeg", "-i", "/music library/file.mp3", "-b:a", "123k", "mp3", "-"}))
})
Context("when command has time offset param", func() {
It("creates a valid command line with offset", func() {
args := createFFmpegCommand("ffmpeg -i %s -b:a %bk -ss %t mp3 -", "/music library/file.mp3", 123, 456)
Expect(args).To(Equal([]string{"ffmpeg", "-i", "/music library/file.mp3", "-b:a", "123k", "-ss", "456", "mp3", "-"}))
})
})
Context("when command does not have time offset param", func() {
It("adds time offset after the input file name", func() {
args := createFFmpegCommand("ffmpeg -i %s -b:a %bk mp3 -", "/music library/file.mp3", 123, 456)
Expect(args).To(Equal([]string{"ffmpeg", "-i", "/music library/file.mp3", "-ss", "456", "-b:a", "123k", "mp3", "-"}))
})
})
})
Describe("createProbeCommand", func() {

View File

@@ -19,8 +19,8 @@ import (
)
type MediaStreamer interface {
NewStream(ctx context.Context, id string, reqFormat string, reqBitRate int) (*Stream, error)
DoStream(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int) (*Stream, error)
NewStream(ctx context.Context, id string, reqFormat string, reqBitRate int, offset int) (*Stream, error)
DoStream(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int, reqOffset int) (*Stream, error)
}
type TranscodingCache cache.FileCache
@@ -40,22 +40,23 @@ type streamJob struct {
mf *model.MediaFile
format string
bitRate int
offset int
}
func (j *streamJob) Key() string {
return fmt.Sprintf("%s.%s.%d.%s", j.mf.ID, j.mf.UpdatedAt.Format(time.RFC3339Nano), j.bitRate, j.format)
return fmt.Sprintf("%s.%s.%d.%s.%d", j.mf.ID, j.mf.UpdatedAt.Format(time.RFC3339Nano), j.bitRate, j.format, j.offset)
}
func (ms *mediaStreamer) NewStream(ctx context.Context, id string, reqFormat string, reqBitRate int) (*Stream, error) {
func (ms *mediaStreamer) NewStream(ctx context.Context, id string, reqFormat string, reqBitRate int, reqOffset int) (*Stream, error) {
mf, err := ms.ds.MediaFile(ctx).Get(id)
if err != nil {
return nil, err
}
return ms.DoStream(ctx, mf, reqFormat, reqBitRate)
return ms.DoStream(ctx, mf, reqFormat, reqBitRate, reqOffset)
}
func (ms *mediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int) (*Stream, error) {
func (ms *mediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int, reqOffset int) (*Stream, error) {
var format string
var bitRate int
var cached bool
@@ -70,7 +71,7 @@ func (ms *mediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, reqF
if format == "raw" {
log.Debug(ctx, "Streaming RAW file", "id", mf.ID, "path", mf.Path,
"requestBitrate", reqBitRate, "requestFormat", reqFormat,
"requestBitrate", reqBitRate, "requestFormat", reqFormat, "requestOffset", reqOffset,
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix,
"selectedBitrate", bitRate, "selectedFormat", format)
f, err := os.Open(mf.Path)
@@ -88,6 +89,7 @@ func (ms *mediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, reqF
mf: mf,
format: format,
bitRate: bitRate,
offset: reqOffset,
}
r, err := ms.cache.Get(ctx, job)
if err != nil {
@@ -100,7 +102,7 @@ func (ms *mediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, reqF
s.Seeker = r.Seeker
log.Debug(ctx, "Streaming TRANSCODED file", "id", mf.ID, "path", mf.Path,
"requestBitrate", reqBitRate, "requestFormat", reqFormat,
"requestBitrate", reqBitRate, "requestFormat", reqFormat, "requestOffset", reqOffset,
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix,
"selectedBitrate", bitRate, "selectedFormat", format, "cached", cached, "seekable", s.Seekable())
@@ -129,11 +131,11 @@ func (s *Stream) EstimatedContentLength() int {
func selectTranscodingOptions(ctx context.Context, ds model.DataStore, mf *model.MediaFile, reqFormat string, reqBitRate int) (format string, bitRate int) {
format = "raw"
if reqFormat == "raw" {
return
return format, 0
}
if reqFormat == mf.Suffix && reqBitRate == 0 {
bitRate = mf.BitRate
return
return format, bitRate
}
trc, hasDefault := request.TranscodingFrom(ctx)
var cFormat string
@@ -159,7 +161,7 @@ func selectTranscodingOptions(ctx context.Context, ds model.DataStore, mf *model
cBitRate = reqBitRate
}
if cBitRate == 0 && cFormat == "" {
return
return format, bitRate
}
t, err := ds.Transcoding(ctx).FindByFormat(cFormat)
if err == nil {
@@ -184,22 +186,26 @@ var (
func GetTranscodingCache() TranscodingCache {
onceTranscodingCache.Do(func() {
instanceTranscodingCache = cache.NewFileCache("Transcoding", conf.Server.TranscodingCacheSize,
consts.TranscodingCacheDir, consts.DefaultTranscodingCacheMaxItems,
func(ctx context.Context, arg cache.Item) (io.Reader, error) {
job := arg.(*streamJob)
t, err := job.ms.ds.Transcoding(ctx).FindByFormat(job.format)
if err != nil {
log.Error(ctx, "Error loading transcoding command", "format", job.format, err)
return nil, os.ErrInvalid
}
out, err := job.ms.transcoder.Transcode(ctx, t.Command, job.mf.Path, job.bitRate)
if err != nil {
log.Error(ctx, "Error starting transcoder", "id", job.mf.ID, err)
return nil, os.ErrInvalid
}
return out, nil
})
instanceTranscodingCache = NewTranscodingCache()
})
return instanceTranscodingCache
}
func NewTranscodingCache() TranscodingCache {
return cache.NewFileCache("Transcoding", conf.Server.TranscodingCacheSize,
consts.TranscodingCacheDir, consts.DefaultTranscodingCacheMaxItems,
func(ctx context.Context, arg cache.Item) (io.Reader, error) {
job := arg.(*streamJob)
t, err := job.ms.ds.Transcoding(ctx).FindByFormat(job.format)
if err != nil {
log.Error(ctx, "Error loading transcoding command", "format", job.format, err)
return nil, os.ErrInvalid
}
out, err := job.ms.transcoder.Transcode(ctx, t.Command, job.mf.Path, job.bitRate, job.offset)
if err != nil {
log.Error(ctx, "Error starting transcoder", "id", job.mf.ID, err)
return nil, os.ErrInvalid
}
return out, nil
})
}

View File

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

View File

@@ -23,50 +23,50 @@ var _ = Describe("MediaStreamer", func() {
BeforeEach(func() {
DeferCleanup(configtest.SetupConfig())
conf.Server.DataFolder, _ = os.MkdirTemp("", "file_caches")
conf.Server.CacheFolder, _ = os.MkdirTemp("", "file_caches")
conf.Server.TranscodingCacheSize = "100MB"
ds = &tests.MockDataStore{MockedTranscoding: &tests.MockTranscodingRepo{}}
ds.MediaFile(ctx).(*tests.MockMediaFileRepo).SetData(model.MediaFiles{
{ID: "123", Path: "tests/fixtures/test.mp3", Suffix: "mp3", BitRate: 128, Duration: 257.0},
})
testCache := core.GetTranscodingCache()
testCache := core.NewTranscodingCache()
Eventually(func() bool { return testCache.Available(context.TODO()) }).Should(BeTrue())
streamer = core.NewMediaStreamer(ds, ffmpeg, testCache)
})
AfterEach(func() {
_ = os.RemoveAll(conf.Server.DataFolder)
_ = os.RemoveAll(conf.Server.CacheFolder)
})
Context("NewStream", func() {
It("returns a seekable stream if format is 'raw'", func() {
s, err := streamer.NewStream(ctx, "123", "raw", 0)
s, err := streamer.NewStream(ctx, "123", "raw", 0, 0)
Expect(err).ToNot(HaveOccurred())
Expect(s.Seekable()).To(BeTrue())
})
It("returns a seekable stream if maxBitRate is 0", func() {
s, err := streamer.NewStream(ctx, "123", "mp3", 0)
s, err := streamer.NewStream(ctx, "123", "mp3", 0, 0)
Expect(err).ToNot(HaveOccurred())
Expect(s.Seekable()).To(BeTrue())
})
It("returns a seekable stream if maxBitRate is higher than file bitRate", func() {
s, err := streamer.NewStream(ctx, "123", "mp3", 320)
s, err := streamer.NewStream(ctx, "123", "mp3", 320, 0)
Expect(err).ToNot(HaveOccurred())
Expect(s.Seekable()).To(BeTrue())
})
It("returns a NON seekable stream if transcode is required", func() {
s, err := streamer.NewStream(ctx, "123", "mp3", 64)
s, err := streamer.NewStream(ctx, "123", "mp3", 64, 0)
Expect(err).To(BeNil())
Expect(s.Seekable()).To(BeFalse())
Expect(s.Duration()).To(Equal(float32(257.0)))
})
It("returns a seekable stream if the file is complete in the cache", func() {
s, err := streamer.NewStream(ctx, "123", "mp3", 32)
s, err := streamer.NewStream(ctx, "123", "mp3", 32, 0)
Expect(err).To(BeNil())
_, _ = io.ReadAll(s)
_ = s.Close()
Eventually(func() bool { return ffmpeg.IsClosed() }, "3s").Should(BeTrue())
s, err = streamer.NewStream(ctx, "123", "mp3", 32)
s, err = streamer.NewStream(ctx, "123", "mp3", 32, 0)
Expect(err).To(BeNil())
Expect(s.Seekable()).To(BeTrue())
})

299
core/playback/device.go Normal file
View File

@@ -0,0 +1,299 @@
package playback
import (
"context"
"errors"
"fmt"
"sync"
"github.com/navidrome/navidrome/core/playback/mpv"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
)
type Track interface {
IsPlaying() bool
SetVolume(value float32) // Used to control the playback volume. A float value between 0.0 and 1.0.
Pause()
Unpause()
Position() int
SetPosition(offset int) error
Close()
String() string
}
type playbackDevice struct {
serviceCtx context.Context
ParentPlaybackServer PlaybackServer
Default bool
User string
Name string
DeviceName string
PlaybackQueue *Queue
Gain float32
PlaybackDone chan bool
ActiveTrack Track
startTrackSwitcher sync.Once
}
type DeviceStatus struct {
CurrentIndex int
Playing bool
Gain float32
Position int
}
const DefaultGain float32 = 1.0
func (pd *playbackDevice) getStatus() DeviceStatus {
pos := 0
if pd.ActiveTrack != nil {
pos = pd.ActiveTrack.Position()
}
return DeviceStatus{
CurrentIndex: pd.PlaybackQueue.Index,
Playing: pd.isPlaying(),
Gain: pd.Gain,
Position: pos,
}
}
// NewPlaybackDevice creates a new playback device which implements all the basic Jukebox mode commands defined here:
// http://www.subsonic.org/pages/api.jsp#jukeboxControl
// Starts the trackSwitcher goroutine for the device.
func NewPlaybackDevice(ctx context.Context, playbackServer PlaybackServer, name string, deviceName string) *playbackDevice {
return &playbackDevice{
serviceCtx: ctx,
ParentPlaybackServer: playbackServer,
User: "",
Name: name,
DeviceName: deviceName,
Gain: DefaultGain,
PlaybackQueue: NewQueue(),
PlaybackDone: make(chan bool),
}
}
func (pd *playbackDevice) String() string {
return fmt.Sprintf("Name: %s, Gain: %.4f, Loaded track: %s", pd.Name, pd.Gain, pd.ActiveTrack)
}
func (pd *playbackDevice) Get(ctx context.Context) (model.MediaFiles, DeviceStatus, error) {
log.Debug(ctx, "Processing Get action", "device", pd)
return pd.PlaybackQueue.Get(), pd.getStatus(), nil
}
func (pd *playbackDevice) Status(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, fmt.Sprintf("processing Status action on: %s, queue: %s", pd, pd.PlaybackQueue))
return pd.getStatus(), nil
}
// Set is similar to a clear followed by a add, but will not change the currently playing track.
func (pd *playbackDevice) Set(ctx context.Context, ids []string) (DeviceStatus, error) {
log.Debug(ctx, "Processing Set action", "ids", ids, "device", pd)
_, err := pd.Clear(ctx)
if err != nil {
log.Error(ctx, "error setting tracks", ids)
return pd.getStatus(), err
}
return pd.Add(ctx, ids)
}
func (pd *playbackDevice) Start(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, "Processing Start action", "device", pd)
pd.startTrackSwitcher.Do(func() {
log.Info(ctx, "Starting trackSwitcher goroutine")
// Start one trackSwitcher goroutine with each device
go func() {
pd.trackSwitcherGoroutine()
}()
})
if pd.ActiveTrack != nil {
if pd.isPlaying() {
log.Debug("trying to start an already playing track")
} else {
pd.ActiveTrack.Unpause()
}
} else {
if !pd.PlaybackQueue.IsEmpty() {
err := pd.switchActiveTrackByIndex(pd.PlaybackQueue.Index)
if err != nil {
return pd.getStatus(), err
}
pd.ActiveTrack.Unpause()
}
}
return pd.getStatus(), nil
}
func (pd *playbackDevice) Stop(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, "Processing Stop action", "device", pd)
if pd.ActiveTrack != nil {
pd.ActiveTrack.Pause()
}
return pd.getStatus(), nil
}
func (pd *playbackDevice) Skip(ctx context.Context, index int, offset int) (DeviceStatus, error) {
log.Debug(ctx, "Processing Skip action", "index", index, "offset", offset, "device", pd)
wasPlaying := pd.isPlaying()
if pd.ActiveTrack != nil && wasPlaying {
pd.ActiveTrack.Pause()
}
if index != pd.PlaybackQueue.Index && pd.ActiveTrack != nil {
pd.ActiveTrack.Close()
pd.ActiveTrack = nil
}
if pd.ActiveTrack == nil {
err := pd.switchActiveTrackByIndex(index)
if err != nil {
return pd.getStatus(), err
}
}
err := pd.ActiveTrack.SetPosition(offset)
if err != nil {
log.Error(ctx, "error setting position", err)
return pd.getStatus(), err
}
if wasPlaying {
_, err = pd.Start(ctx)
if err != nil {
log.Error(ctx, "error starting new track after skipping")
return pd.getStatus(), err
}
}
return pd.getStatus(), nil
}
func (pd *playbackDevice) Add(ctx context.Context, ids []string) (DeviceStatus, error) {
log.Debug(ctx, "Processing Add action", "ids", ids, "device", pd)
if len(ids) < 1 {
return pd.getStatus(), nil
}
items := model.MediaFiles{}
for _, id := range ids {
mf, err := pd.ParentPlaybackServer.GetMediaFile(id)
if err != nil {
return DeviceStatus{}, err
}
log.Debug(ctx, "Found mediafile: "+mf.Path)
items = append(items, *mf)
}
pd.PlaybackQueue.Add(items)
return pd.getStatus(), nil
}
func (pd *playbackDevice) Clear(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, "Processing Clear action", "device", pd)
if pd.ActiveTrack != nil {
pd.ActiveTrack.Pause()
pd.ActiveTrack.Close()
pd.ActiveTrack = nil
}
pd.PlaybackQueue.Clear()
return pd.getStatus(), nil
}
func (pd *playbackDevice) Remove(ctx context.Context, index int) (DeviceStatus, error) {
log.Debug(ctx, "Processing Remove action", "index", index, "device", pd)
// pausing if attempting to remove running track
if pd.isPlaying() && pd.PlaybackQueue.Index == index {
_, err := pd.Stop(ctx)
if err != nil {
log.Error(ctx, "error stopping running track")
return pd.getStatus(), err
}
}
if index > -1 && index < pd.PlaybackQueue.Size() {
pd.PlaybackQueue.Remove(index)
} else {
log.Error(ctx, "Index to remove out of range: "+fmt.Sprint(index))
}
return pd.getStatus(), nil
}
func (pd *playbackDevice) Shuffle(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, "Processing Shuffle action", "device", pd)
if pd.PlaybackQueue.Size() > 1 {
pd.PlaybackQueue.Shuffle()
}
return pd.getStatus(), nil
}
// SetGain is used to control the playback volume. A float value between 0.0 and 1.0.
func (pd *playbackDevice) SetGain(ctx context.Context, gain float32) (DeviceStatus, error) {
log.Debug(ctx, "Processing SetGain action", "newGain", gain, "device", pd)
if pd.ActiveTrack != nil {
pd.ActiveTrack.SetVolume(gain)
}
pd.Gain = gain
return pd.getStatus(), nil
}
func (pd *playbackDevice) isPlaying() bool {
return pd.ActiveTrack != nil && pd.ActiveTrack.IsPlaying()
}
func (pd *playbackDevice) trackSwitcherGoroutine() {
log.Debug("Started trackSwitcher goroutine", "device", pd)
for {
select {
case <-pd.PlaybackDone:
log.Debug("Track switching detected")
if pd.ActiveTrack != nil {
pd.ActiveTrack.Close()
pd.ActiveTrack = nil
}
if !pd.PlaybackQueue.IsAtLastElement() {
pd.PlaybackQueue.IncreaseIndex()
log.Debug("Switching to next song", "queue", pd.PlaybackQueue.String())
err := pd.switchActiveTrackByIndex(pd.PlaybackQueue.Index)
if err != nil {
log.Error("Error switching track", err)
}
if pd.ActiveTrack != nil {
pd.ActiveTrack.Unpause()
}
} else {
log.Debug("There is no song left in the playlist. Finish.")
}
case <-pd.serviceCtx.Done():
log.Debug("Stopping trackSwitcher goroutine", "device", pd.Name)
return
}
}
}
func (pd *playbackDevice) switchActiveTrackByIndex(index int) error {
pd.PlaybackQueue.SetIndex(index)
currentTrack := pd.PlaybackQueue.Current()
if currentTrack == nil {
return errors.New("could not get current track")
}
track, err := mpv.NewTrack(pd.serviceCtx, pd.PlaybackDone, pd.DeviceName, *currentTrack)
if err != nil {
return err
}
pd.ActiveTrack = track
pd.ActiveTrack.SetVolume(pd.Gain)
return nil
}

123
core/playback/mpv/mpv.go Normal file
View File

@@ -0,0 +1,123 @@
package mpv
import (
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
"sync"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
)
func start(ctx context.Context, args []string) (Executor, error) {
log.Debug("Executing mpv command", "cmd", args)
j := Executor{args: args}
j.PipeReader, j.out = io.Pipe()
err := j.start(ctx)
if err != nil {
return Executor{}, err
}
go j.wait()
return j, nil
}
func (j *Executor) Cancel() error {
if j.cmd != nil {
return j.cmd.Cancel()
}
return fmt.Errorf("there is non command to cancel")
}
type Executor struct {
*io.PipeReader
out *io.PipeWriter
args []string
cmd *exec.Cmd
}
func (j *Executor) start(ctx context.Context) error {
cmd := exec.CommandContext(ctx, j.args[0], j.args[1:]...) // #nosec
cmd.Stdout = j.out
if log.IsGreaterOrEqualTo(log.LevelTrace) {
cmd.Stderr = os.Stderr
} else {
cmd.Stderr = io.Discard
}
j.cmd = cmd
if err := cmd.Start(); err != nil {
return fmt.Errorf("starting cmd: %w", err)
}
return nil
}
func (j *Executor) wait() {
if err := j.cmd.Wait(); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
_ = j.out.CloseWithError(fmt.Errorf("%s exited with non-zero status code: %d", j.args[0], exitErr.ExitCode()))
} else {
_ = j.out.CloseWithError(fmt.Errorf("waiting %s cmd: %w", j.args[0], err))
}
return
}
_ = j.out.Close()
}
// Path will always be an absolute path
func createMPVCommand(deviceName string, filename string, socketName string) []string {
split := strings.Split(fixCmd(conf.Server.MPVCmdTemplate), " ")
for i, s := range split {
s = strings.ReplaceAll(s, "%d", deviceName)
s = strings.ReplaceAll(s, "%f", filename)
s = strings.ReplaceAll(s, "%s", socketName)
split[i] = s
}
return split
}
func fixCmd(cmd string) string {
split := strings.Split(cmd, " ")
var result []string
cmdPath, _ := mpvCommand()
for _, s := range split {
if s == "mpv" || s == "mpv.exe" {
result = append(result, cmdPath)
} else {
result = append(result, s)
}
}
return strings.Join(result, " ")
}
// This is a 1:1 copy of the stuff in ffmpeg.go, need to be unified.
func mpvCommand() (string, error) {
mpvOnce.Do(func() {
if conf.Server.MPVPath != "" {
mpvPath = conf.Server.MPVPath
mpvPath, mpvErr = exec.LookPath(mpvPath)
} else {
mpvPath, mpvErr = exec.LookPath("mpv")
if errors.Is(mpvErr, exec.ErrDot) {
log.Trace("mpv found in current folder '.'")
mpvPath, mpvErr = exec.LookPath("./mpv")
}
}
if mpvErr == nil {
log.Info("Found mpv", "path", mpvPath)
return
}
})
return mpvPath, mpvErr
}
var (
mpvOnce sync.Once
mpvPath string
mpvErr error
)

View File

@@ -0,0 +1,22 @@
//go:build !windows
package mpv
import (
"os"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/utils"
)
func socketName(prefix, suffix string) string {
return utils.TempFileName(prefix, suffix)
}
func removeSocket(socketName string) {
log.Debug("Removing socketfile", "socketfile", socketName)
err := os.Remove(socketName)
if err != nil {
log.Error("Error cleaning up socketfile", "socketfile", socketName, err)
}
}

View File

@@ -0,0 +1,19 @@
//go:build windows
package mpv
import (
"path/filepath"
"github.com/google/uuid"
)
func socketName(prefix, suffix string) string {
// Windows needs to use a named pipe for the socket
// see https://mpv.io/manual/master#using-mpv-from-other-programs-or-scripts
return filepath.Join(`\\.\pipe\mpvsocket`, prefix+uuid.NewString()+suffix)
}
func removeSocket(string) {
// Windows automatically handles cleaning up named pipe
}

220
core/playback/mpv/track.go Normal file
View File

@@ -0,0 +1,220 @@
package mpv
// Audio-playback using mpv media-server. See mpv.io
// https://github.com/dexterlb/mpvipc
// https://mpv.io/manual/master/#json-ipc
// https://mpv.io/manual/master/#properties
import (
"context"
"fmt"
"os"
"time"
"github.com/dexterlb/mpvipc"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
)
type MpvTrack struct {
MediaFile model.MediaFile
PlaybackDone chan bool
Conn *mpvipc.Connection
IPCSocketName string
Exe *Executor
CloseCalled bool
}
func NewTrack(ctx context.Context, playbackDoneChannel chan bool, deviceName string, mf model.MediaFile) (*MpvTrack, error) {
log.Debug("Loading track", "trackPath", mf.Path, "mediaType", mf.ContentType())
if _, err := mpvCommand(); err != nil {
return nil, err
}
tmpSocketName := socketName("mpv-ctrl-", ".socket")
args := createMPVCommand(deviceName, mf.Path, tmpSocketName)
exe, err := start(ctx, args)
if err != nil {
log.Error("Error starting mpv process", err)
return nil, err
}
// wait for socket to show up
err = waitForSocket(tmpSocketName, 3*time.Second, 100*time.Millisecond)
if err != nil {
log.Error("Error or timeout waiting for control socket", "socketname", tmpSocketName, err)
return nil, err
}
conn := mpvipc.NewConnection(tmpSocketName)
err = conn.Open()
if err != nil {
log.Error("Error opening new connection", err)
return nil, err
}
theTrack := &MpvTrack{MediaFile: mf, PlaybackDone: playbackDoneChannel, Conn: conn, IPCSocketName: tmpSocketName, Exe: &exe, CloseCalled: false}
go func() {
conn.WaitUntilClosed()
log.Info("Hitting end-of-stream, signalling on channel")
if !theTrack.CloseCalled {
playbackDoneChannel <- true
}
}()
return theTrack, nil
}
func (t *MpvTrack) String() string {
return fmt.Sprintf("Name: %s, Socket: %s", t.MediaFile.Path, t.IPCSocketName)
}
// Used to control the playback volume. A float value between 0.0 and 1.0.
func (t *MpvTrack) SetVolume(value float32) {
// mpv's volume as described in the --volume parameter:
// Set the startup volume. 0 means silence, 100 means no volume reduction or amplification.
// Negative values can be passed for compatibility, but are treated as 0.
log.Debug("Setting volume", "volume", value, "track", t)
vol := int(value * 100)
err := t.Conn.Set("volume", vol)
if err != nil {
log.Error("Error setting volume", "volume", value, "track", t, err)
}
}
func (t *MpvTrack) Unpause() {
log.Debug("Unpausing track", "track", t)
err := t.Conn.Set("pause", false)
if err != nil {
log.Error("Error unpausing track", "track", t, err)
}
}
func (t *MpvTrack) Pause() {
log.Debug("Pausing track", "track", t)
err := t.Conn.Set("pause", true)
if err != nil {
log.Error("Error pausing track", "track", t, err)
}
}
func (t *MpvTrack) Close() {
log.Debug("Closing resources", "track", t)
t.CloseCalled = true
// trying to shutdown mpv process using socket
if t.isSocketFilePresent() {
log.Debug("sending shutdown command")
_, err := t.Conn.Call("quit")
if err != nil {
log.Warn("Error sending quit command to mpv-ipc socket", err)
if t.Exe != nil {
log.Debug("cancelling executor")
err = t.Exe.Cancel()
if err != nil {
log.Warn("Error canceling executor", err)
}
}
}
}
if t.isSocketFilePresent() {
removeSocket(t.IPCSocketName)
}
}
func (t *MpvTrack) isSocketFilePresent() bool {
if len(t.IPCSocketName) < 1 {
return false
}
fileInfo, err := os.Stat(t.IPCSocketName)
return err == nil && fileInfo != nil && !fileInfo.IsDir()
}
// Position returns the playback position in seconds.
// Every now and then the mpv IPC interface returns "mpv error: property unavailable"
// in this case we have to retry
func (t *MpvTrack) Position() int {
retryCount := 0
for {
position, err := t.Conn.Get("time-pos")
if err != nil && err.Error() == "mpv error: property unavailable" {
retryCount += 1
log.Debug("Got mpv error, retrying...", "retries", retryCount, err)
if retryCount > 5 {
return 0
}
break
}
if err != nil {
log.Error("Error getting position in track", "track", t, err)
return 0
}
pos, ok := position.(float64)
if !ok {
log.Error("Could not cast position from mpv into float64", "position", position, "track", t)
return 0
} else {
return int(pos)
}
}
return 0
}
func (t *MpvTrack) SetPosition(offset int) error {
log.Debug("Setting position", "offset", offset, "track", t)
pos := t.Position()
if pos == offset {
log.Debug("No position difference, skipping operation", "track", t)
return nil
}
err := t.Conn.Set("time-pos", float64(offset))
if err != nil {
log.Error("Could not set the position in track", "track", t, "offset", offset, err)
return err
}
return nil
}
func (t *MpvTrack) IsPlaying() bool {
log.Debug("Checking if track is playing", "track", t)
pausing, err := t.Conn.Get("pause")
if err != nil {
log.Error("Problem getting paused status", "track", t, err)
return false
}
pause, ok := pausing.(bool)
if !ok {
log.Error("Could not cast pausing to boolean", "track", t, "value", pausing)
return false
}
return !pause
}
func waitForSocket(path string, timeout time.Duration, pause time.Duration) error {
start := time.Now()
end := start.Add(timeout)
var retries int = 0
for {
fileInfo, err := os.Stat(path)
if err == nil && fileInfo != nil && !fileInfo.IsDir() {
log.Debug("Socket found", "retries", retries, "waitTime", time.Since(start))
return nil
}
if time.Now().After(end) {
return fmt.Errorf("timeout reached: %s", timeout)
}
time.Sleep(pause)
retries += 1
}
}

View File

@@ -0,0 +1,17 @@
package playback
import (
"testing"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestPlayback(t *testing.T) {
tests.Init(t, false)
log.SetLevel(log.LevelFatal)
RegisterFailHandler(Fail)
RunSpecs(t, "Playback Suite")
}

View File

@@ -0,0 +1,127 @@
// Package playback implements audio playback using PlaybackDevices. It is used to implement the Jukebox mode in turn.
// It makes use of the MPV library to do the playback. Major parts are:
// - decoder which includes decoding and transcoding of various audio file formats
// - device implementing the basic functions to work with audio devices like set, play, stop, skip, ...
// - queue a simple playlist
package playback
import (
"context"
"fmt"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/singleton"
)
type PlaybackServer interface {
Run(ctx context.Context) error
GetDeviceForUser(user string) (*playbackDevice, error)
GetMediaFile(id string) (*model.MediaFile, error)
}
type playbackServer struct {
ctx *context.Context
datastore model.DataStore
playbackDevices []playbackDevice
}
// GetInstance returns the playback-server singleton
func GetInstance(ds model.DataStore) PlaybackServer {
return singleton.GetInstance(func() *playbackServer {
return &playbackServer{datastore: ds}
})
}
// Run starts the playback server which serves request until canceled using the given context
func (ps *playbackServer) Run(ctx context.Context) error {
ps.ctx = &ctx
devices, err := ps.initDeviceStatus(ctx, conf.Server.Jukebox.Devices, conf.Server.Jukebox.Default)
if err != nil {
return err
}
ps.playbackDevices = devices
log.Info(ctx, fmt.Sprintf("%d audio devices found", len(devices)))
defaultDevice, _ := ps.getDefaultDevice()
log.Info(ctx, "Using audio device: "+defaultDevice.DeviceName)
<-ctx.Done()
// Should confirm all subprocess are terminated before returning
return nil
}
func (ps *playbackServer) initDeviceStatus(ctx context.Context, devices []conf.AudioDeviceDefinition, defaultDevice string) ([]playbackDevice, error) {
pbDevices := make([]playbackDevice, max(1, len(devices)))
defaultDeviceFound := false
if defaultDevice == "" {
// if there are no devices given and no default device, we create a synthetic device named "auto"
if len(devices) == 0 {
pbDevices[0] = *NewPlaybackDevice(ctx, ps, "auto", "auto")
}
// if there is but only one entry and no default given, just use that.
if len(devices) == 1 {
if len(devices[0]) != 2 {
return []playbackDevice{}, fmt.Errorf("audio device definition ought to contain 2 fields, found: %d ", len(devices[0]))
}
pbDevices[0] = *NewPlaybackDevice(ctx, ps, devices[0][0], devices[0][1])
}
if len(devices) > 1 {
return []playbackDevice{}, fmt.Errorf("number of audio device found is %d, but no default device defined. Set Jukebox.Default", len(devices))
}
pbDevices[0].Default = true
return pbDevices, nil
}
for idx, audioDevice := range devices {
if len(audioDevice) != 2 {
return []playbackDevice{}, fmt.Errorf("audio device definition ought to contain 2 fields, found: %d ", len(audioDevice))
}
pbDevices[idx] = *NewPlaybackDevice(ctx, ps, audioDevice[0], audioDevice[1])
if audioDevice[0] == defaultDevice {
pbDevices[idx].Default = true
defaultDeviceFound = true
}
}
if !defaultDeviceFound {
return []playbackDevice{}, fmt.Errorf("default device name not found: %s ", defaultDevice)
}
return pbDevices, nil
}
func (ps *playbackServer) getDefaultDevice() (*playbackDevice, error) {
for idx := range ps.playbackDevices {
if ps.playbackDevices[idx].Default {
return &ps.playbackDevices[idx], nil
}
}
return nil, fmt.Errorf("no default device found")
}
// GetMediaFile retrieves the MediaFile given by the id parameter
func (ps *playbackServer) GetMediaFile(id string) (*model.MediaFile, error) {
return ps.datastore.MediaFile(*ps.ctx).Get(id)
}
// GetDeviceForUser returns the audio playback device for the given user. As of now this is but only the default device.
func (ps *playbackServer) GetDeviceForUser(user string) (*playbackDevice, error) {
log.Debug("Processing GetDevice", "user", user)
// README: here we might plug-in the user-device mapping one fine day
device, err := ps.getDefaultDevice()
if err != nil {
return nil, err
}
device.User = user
return device, nil
}

136
core/playback/queue.go Normal file
View File

@@ -0,0 +1,136 @@
package playback
import (
"fmt"
"math/rand"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
)
type Queue struct {
Index int
Items model.MediaFiles
}
func NewQueue() *Queue {
return &Queue{
Index: -1,
Items: model.MediaFiles{},
}
}
func (pd *Queue) String() string {
filenames := ""
for idx, item := range pd.Items {
filenames += fmt.Sprint(idx) + ":" + item.Path + " "
}
return fmt.Sprintf("#Items: %d, idx: %d, files: %s", len(pd.Items), pd.Index, filenames)
}
// returns the current mediafile or nil
func (pd *Queue) Current() *model.MediaFile {
if pd.Index == -1 {
return nil
}
if pd.Index >= len(pd.Items) {
log.Error("internal error: current song index out of bounds", "idx", pd.Index, "length", len(pd.Items))
return nil
}
return &pd.Items[pd.Index]
}
// returns the whole queue
func (pd *Queue) Get() model.MediaFiles {
return pd.Items
}
func (pd *Queue) Size() int {
return len(pd.Items)
}
func (pd *Queue) IsEmpty() bool {
return len(pd.Items) < 1
}
// set is similar to a clear followed by a add, but will not change the currently playing track.
func (pd *Queue) Set(items model.MediaFiles) {
pd.Clear()
pd.Items = append(pd.Items, items...)
}
// adding mediafiles to the queue
func (pd *Queue) Add(items model.MediaFiles) {
pd.Items = append(pd.Items, items...)
if pd.Index == -1 && len(pd.Items) > 0 {
pd.Index = 0
}
}
// empties whole queue
func (pd *Queue) Clear() {
pd.Index = -1
pd.Items = nil
}
// idx Zero-based index of the song to skip to or remove.
func (pd *Queue) Remove(idx int) {
current := pd.Current()
backupID := ""
if current != nil {
backupID = current.ID
}
pd.Items = append(pd.Items[:idx], pd.Items[idx+1:]...)
var err error
pd.Index, err = pd.getMediaFileIndexByID(backupID)
if err != nil {
// we seem to have deleted the current id, setting to default:
pd.Index = -1
}
}
func (pd *Queue) Shuffle() {
current := pd.Current()
backupID := ""
if current != nil {
backupID = current.ID
}
rand.Shuffle(len(pd.Items), func(i, j int) { pd.Items[i], pd.Items[j] = pd.Items[j], pd.Items[i] })
var err error
pd.Index, err = pd.getMediaFileIndexByID(backupID)
if err != nil {
log.Error("Could not find ID while shuffling: " + backupID)
}
}
func (pd *Queue) getMediaFileIndexByID(id string) (int, error) {
for idx, item := range pd.Items {
if item.ID == id {
return idx, nil
}
}
return -1, fmt.Errorf("ID not found in playlist: " + id)
}
// Sets the index to a new, valid value inside the Items. Values lower than zero are going to be zero,
// values above will be limited by number of items.
func (pd *Queue) SetIndex(idx int) {
pd.Index = max(0, min(idx, len(pd.Items)-1))
}
// Are we at the last track?
func (pd *Queue) IsAtLastElement() bool {
return (pd.Index + 1) >= len(pd.Items)
}
// Goto next index
func (pd *Queue) IncreaseIndex() {
if !pd.IsAtLastElement() {
pd.SetIndex(pd.Index + 1)
}
}

121
core/playback/queue_test.go Normal file
View File

@@ -0,0 +1,121 @@
package playback
import (
"github.com/navidrome/navidrome/model"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Queues", func() {
var queue *Queue
BeforeEach(func() {
queue = NewQueue()
})
Describe("use empty queue", func() {
It("is empty", func() {
Expect(queue.Items).To(BeEmpty())
Expect(queue.Index).To(Equal(-1))
})
})
Describe("Operate on small queue", func() {
BeforeEach(func() {
mfs := model.MediaFiles{
{
ID: "1", Artist: "Queen", Compilation: false, Path: "/music1/hammer.mp3",
},
{
ID: "2", Artist: "Vinyard Rose", Compilation: false, Path: "/music1/cassidy.mp3",
},
}
queue.Add(mfs)
})
It("contains the preloaded data", func() {
Expect(queue.Get).ToNot(BeNil())
Expect(queue.Size()).To(Equal(2))
})
It("could read data by ID", func() {
idx, err := queue.getMediaFileIndexByID("1")
Expect(err).ToNot(HaveOccurred())
Expect(idx).ToNot(BeNil())
Expect(idx).To(Equal(0))
queue.SetIndex(idx)
mf := queue.Current()
Expect(mf).ToNot(BeNil())
Expect(mf.ID).To(Equal("1"))
Expect(mf.Artist).To(Equal("Queen"))
Expect(mf.Path).To(Equal("/music1/hammer.mp3"))
})
})
Describe("Read/Write operations", func() {
BeforeEach(func() {
mfs := model.MediaFiles{
{
ID: "1", Artist: "Queen", Compilation: false, Path: "/music1/hammer.mp3",
},
{
ID: "2", Artist: "Vinyard Rose", Compilation: false, Path: "/music1/cassidy.mp3",
},
{
ID: "3", Artist: "Pink Floyd", Compilation: false, Path: "/music1/time.mp3",
},
{
ID: "4", Artist: "Mike Oldfield", Compilation: false, Path: "/music1/moonlight-shadow.mp3",
},
{
ID: "5", Artist: "Red Hot Chili Peppers", Compilation: false, Path: "/music1/californication.mp3",
},
}
queue.Add(mfs)
})
It("contains the preloaded data", func() {
Expect(queue.Get).ToNot(BeNil())
Expect(queue.Size()).To(Equal(5))
})
It("could read data by ID", func() {
idx, err := queue.getMediaFileIndexByID("5")
Expect(err).ToNot(HaveOccurred())
Expect(idx).ToNot(BeNil())
Expect(idx).To(Equal(4))
queue.SetIndex(idx)
mf := queue.Current()
Expect(mf).ToNot(BeNil())
Expect(mf.ID).To(Equal("5"))
Expect(mf.Artist).To(Equal("Red Hot Chili Peppers"))
Expect(mf.Path).To(Equal("/music1/californication.mp3"))
})
It("could shuffle the data correctly", func() {
queue.Shuffle()
Expect(queue.Size()).To(Equal(5))
})
It("could remove entries correctly", func() {
queue.Remove(0)
Expect(queue.Size()).To(Equal(4))
queue.Remove(3)
Expect(queue.Size()).To(Equal(3))
})
It("clear the whole thing on request", func() {
Expect(queue.Size()).To(Equal(5))
queue.Clear()
Expect(queue.Size()).To(Equal(0))
})
})
})

View File

@@ -14,6 +14,7 @@ import (
"strings"
"time"
"github.com/RaveNoX/go-jsoncommentstrip"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/criteria"
@@ -23,6 +24,7 @@ import (
type Playlists interface {
ImportFile(ctx context.Context, dir string, fname string) (*model.Playlist, error)
Update(ctx context.Context, playlistID string, name *string, comment *string, public *bool, idsToAdd []string, idxToRemove []int) error
ImportM3U(ctx context.Context, reader io.Reader) (*model.Playlist, error)
}
type playlists struct {
@@ -47,6 +49,26 @@ func (s *playlists) ImportFile(ctx context.Context, dir string, fname string) (*
return pls, err
}
func (s *playlists) ImportM3U(ctx context.Context, reader io.Reader) (*model.Playlist, error) {
owner, _ := request.UserFrom(ctx)
pls := &model.Playlist{
OwnerID: owner.ID,
Public: false,
Sync: true,
}
pls, err := s.parseM3U(ctx, pls, "", reader)
if err != nil {
log.Error(ctx, "Error parsing playlist", err)
return nil, err
}
err = s.ds.Playlist(ctx).Put(pls)
if err != nil {
log.Error(ctx, "Error saving playlist", err)
return nil, err
}
return pls, nil
}
func (s *playlists) parsePlaylist(ctx context.Context, playlistFile string, baseDir string) (*model.Playlist, error) {
pls, err := s.newSyncedPlaylist(baseDir, playlistFile)
if err != nil {
@@ -91,7 +113,8 @@ func (s *playlists) newSyncedPlaylist(baseDir string, playlistFile string) (*mod
func (s *playlists) parseNSP(ctx context.Context, pls *model.Playlist, file io.Reader) (*model.Playlist, error) {
nsp := &nspFile{}
dec := json.NewDecoder(file)
reader := jsoncommentstrip.NewReader(file)
dec := json.NewDecoder(reader)
err := dec.Decode(nsp)
if err != nil {
log.Error(ctx, "Error parsing SmartPlaylist", "playlist", pls.Name, err)
@@ -107,31 +130,40 @@ func (s *playlists) parseNSP(ctx context.Context, pls *model.Playlist, file io.R
return pls, nil
}
func (s *playlists) parseM3U(ctx context.Context, pls *model.Playlist, baseDir string, file io.Reader) (*model.Playlist, error) {
func (s *playlists) parseM3U(ctx context.Context, pls *model.Playlist, baseDir string, reader io.Reader) (*model.Playlist, error) {
mediaFileRepository := s.ds.MediaFile(ctx)
scanner := bufio.NewScanner(file)
scanner := bufio.NewScanner(reader)
scanner.Split(scanLines)
var mfs model.MediaFiles
for scanner.Scan() {
path := strings.TrimSpace(scanner.Text())
// Skip empty lines and extended info
if path == "" || strings.HasPrefix(path, "#") {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "#PLAYLIST:") {
if split := strings.Split(line, ":"); len(split) >= 2 {
pls.Name = split[1]
}
continue
}
if strings.HasPrefix(path, "file://") {
path = strings.TrimPrefix(path, "file://")
path, _ = url.QueryUnescape(path)
// Skip empty lines and extended info
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if !filepath.IsAbs(path) {
path = filepath.Join(baseDir, path)
if strings.HasPrefix(line, "file://") {
line = strings.TrimPrefix(line, "file://")
line, _ = url.QueryUnescape(line)
}
mf, err := mediaFileRepository.FindByPath(path)
if baseDir != "" && !filepath.IsAbs(line) {
line = filepath.Join(baseDir, line)
}
mf, err := mediaFileRepository.FindByPath(line)
if err != nil {
log.Warn(ctx, "Path in playlist not found", "playlist", pls.Name, "path", path, err)
log.Warn(ctx, "Path in playlist not found", "playlist", pls.Name, "path", line, err)
continue
}
mfs = append(mfs, *mf)
}
if pls.Name == "" {
pls.Name = time.Now().Format(time.RFC3339)
}
pls.Tracks = nil
pls.AddMediaFiles(mfs)
@@ -157,7 +189,7 @@ func (s *playlists) updatePlaylist(ctx context.Context, newPls *model.Playlist)
newPls.Comment = pls.Comment
newPls.OwnerID = pls.OwnerID
newPls.Public = pls.Public
newPls.EvaluatedAt = time.Time{}
newPls.EvaluatedAt = &time.Time{}
} else {
log.Info(ctx, "Adding synced playlist", "playlist", newPls.Name, "path", newPls.Path, "owner", owner.UserName)
newPls.OwnerID = owner.ID
@@ -199,13 +231,17 @@ func (s *playlists) Update(ctx context.Context, playlistID string,
var pls *model.Playlist
var err error
repo := tx.Playlist(ctx)
tracks := repo.Tracks(playlistID, true)
if tracks == nil {
return fmt.Errorf("%w: playlist '%s'", model.ErrNotFound, playlistID)
}
if needsTrackRefresh {
pls, err = repo.GetWithTracks(playlistID, true)
pls.RemoveTracks(idxToRemove)
pls.AddTracks(idsToAdd)
} else {
if len(idsToAdd) > 0 {
_, err = repo.Tracks(playlistID, true).Add(idsToAdd)
_, err = tracks.Add(idsToAdd)
if err != nil {
return err
}
@@ -232,7 +268,7 @@ func (s *playlists) Update(ctx context.Context, playlistID string,
}
// Special case: The playlist is now empty
if len(idxToRemove) > 0 && len(pls.Tracks) == 0 {
if err = repo.Tracks(playlistID, true).DeleteAll(); err != nil {
if err = tracks.DeleteAll(); err != nil {
return err
}
}

View File

@@ -2,6 +2,11 @@ package core
import (
"context"
"os"
"time"
"github.com/navidrome/navidrome/model/criteria"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/tests"
@@ -12,13 +17,16 @@ import (
var _ = Describe("Playlists", func() {
var ds model.DataStore
var ps Playlists
var mp mockedPlaylist
ctx := context.Background()
BeforeEach(func() {
mp = mockedPlaylist{}
ds = &tests.MockDataStore{
MockedMediaFile: &mockedMediaFile{},
MockedPlaylist: &mockedPlaylist{},
MockedPlaylist: &mp,
}
ctx = request.WithUser(ctx, model.User{ID: "123"})
})
Describe("ImportFile", func() {
@@ -26,27 +34,76 @@ var _ = Describe("Playlists", func() {
ps = NewPlaylists(ds)
})
Describe("M3U", func() {
It("parses well-formed playlists", func() {
pls, err := ps.ImportFile(ctx, "tests/fixtures", "playlists/pls1.m3u")
Expect(err).To(BeNil())
Expect(pls.OwnerID).To(Equal("123"))
Expect(pls.Tracks).To(HaveLen(3))
Expect(pls.Tracks[0].Path).To(Equal("tests/fixtures/test.mp3"))
Expect(pls.Tracks[1].Path).To(Equal("tests/fixtures/test.ogg"))
Expect(pls.Tracks[2].Path).To(Equal("/tests/fixtures/01 Invisible (RED) Edit Version.mp3"))
Expect(mp.last).To(Equal(pls))
})
It("parses playlists using LF ending", func() {
pls, err := ps.ImportFile(ctx, "tests/fixtures/playlists", "lf-ended.m3u")
Expect(err).To(BeNil())
Expect(pls.Tracks).To(HaveLen(2))
})
It("parses playlists using CR ending (old Mac format)", func() {
pls, err := ps.ImportFile(ctx, "tests/fixtures/playlists", "cr-ended.m3u")
Expect(err).To(BeNil())
Expect(pls.Tracks).To(HaveLen(2))
})
})
Describe("NSP", func() {
It("parses well-formed playlists", func() {
pls, err := ps.ImportFile(ctx, "tests/fixtures", "playlists/recently_played.nsp")
Expect(err).To(BeNil())
Expect(mp.last).To(Equal(pls))
Expect(pls.OwnerID).To(Equal("123"))
Expect(pls.Name).To(Equal("Recently Played"))
Expect(pls.Comment).To(Equal("Recently played tracks"))
Expect(pls.Rules.Sort).To(Equal("lastPlayed"))
Expect(pls.Rules.Order).To(Equal("desc"))
Expect(pls.Rules.Limit).To(Equal(100))
Expect(pls.Rules.Expression).To(BeAssignableToTypeOf(criteria.All{}))
})
})
})
Describe("ImportM3U", func() {
BeforeEach(func() {
ps = NewPlaylists(ds)
ctx = request.WithUser(ctx, model.User{ID: "123"})
})
It("parses well-formed playlists", func() {
pls, err := ps.ImportFile(ctx, "tests/fixtures", "playlists/pls1.m3u")
f, _ := os.Open("tests/fixtures/playlists/pls-post-with-name.m3u")
defer f.Close()
pls, err := ps.ImportM3U(ctx, f)
Expect(pls.OwnerID).To(Equal("123"))
Expect(pls.Name).To(Equal("playlist 1"))
Expect(err).To(BeNil())
Expect(pls.Tracks).To(HaveLen(3))
Expect(pls.Tracks[0].Path).To(Equal("tests/fixtures/test.mp3"))
Expect(pls.Tracks[1].Path).To(Equal("tests/fixtures/test.ogg"))
Expect(pls.Tracks[2].Path).To(Equal("/tests/fixtures/01 Invisible (RED) Edit Version.mp3"))
Expect(mp.last).To(Equal(pls))
f.Close()
})
It("parses playlists using LF ending", func() {
pls, err := ps.ImportFile(ctx, "tests/fixtures/playlists", "lf-ended.m3u")
It("sets the playlist name as a timestamp if the #PLAYLIST directive is not present", func() {
f, _ := os.Open("tests/fixtures/playlists/pls-post.m3u")
defer f.Close()
pls, err := ps.ImportM3U(ctx, f)
Expect(err).To(BeNil())
Expect(pls.Tracks).To(HaveLen(2))
})
It("parses playlists using CR ending (old Mac format)", func() {
pls, err := ps.ImportFile(ctx, "tests/fixtures/playlists", "cr-ended.m3u")
_, err = time.Parse(time.RFC3339, pls.Name)
Expect(err).To(BeNil())
Expect(pls.Tracks).To(HaveLen(2))
})
})
})
@@ -62,6 +119,7 @@ func (r *mockedMediaFile) FindByPath(s string) (*model.MediaFile, error) {
}
type mockedPlaylist struct {
last *model.Playlist
model.PlaylistRepository
}
@@ -69,6 +127,7 @@ func (r *mockedPlaylist) FindByPath(string) (*model.Playlist, error) {
return nil, model.ErrNotFound
}
func (r *mockedPlaylist) Put(*model.Playlist) error {
func (r *mockedPlaylist) Put(pls *model.Playlist) error {
r.last = pls
return nil
}

View File

@@ -5,10 +5,9 @@ import (
"sort"
"time"
"github.com/jellydator/ttlcache/v2"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/ReneKroon/ttlcache/v2"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"

View File

@@ -40,16 +40,16 @@ var _ = Describe("PlayTracker", func() {
tracker = newPlayTracker(ds, events.GetBroker())
track = model.MediaFile{
ID: "123",
Title: "Track Title",
Album: "Track Album",
AlbumID: "al-1",
Artist: "Track Artist",
ArtistID: "ar-1",
AlbumArtist: "Track AlbumArtist",
TrackNumber: 1,
Duration: 180,
MbzTrackID: "mbz-123",
ID: "123",
Title: "Track Title",
Album: "Track Album",
AlbumID: "al-1",
Artist: "Track Artist",
ArtistID: "ar-1",
AlbumArtist: "Track AlbumArtist",
TrackNumber: 1,
Duration: 180,
MbzRecordingID: "mbz-123",
}
_ = ds.MediaFile(ctx).Put(&track)
artist = model.Artist{ID: "ar-1"}

View File

@@ -10,6 +10,7 @@ import (
gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
. "github.com/navidrome/navidrome/utils/gg"
"github.com/navidrome/navidrome/utils/slice"
)
@@ -34,10 +35,11 @@ func (s *shareService) Load(ctx context.Context, id string) (*model.Share, error
if err != nil {
return nil, err
}
if !share.ExpiresAt.IsZero() && share.ExpiresAt.Before(time.Now()) {
return nil, model.ErrNotAvailable
expiresAt := V(share.ExpiresAt)
if !expiresAt.IsZero() && expiresAt.Before(time.Now()) {
return nil, model.ErrExpired
}
share.LastVisitedAt = time.Now()
share.LastVisitedAt = P(time.Now())
share.VisitCount++
err = repo.(rest.Persistable).Update(id, share, "last_visited_at", "visit_count")
@@ -90,8 +92,8 @@ func (r *shareRepositoryWrapper) Save(entity interface{}) (string, error) {
return "", err
}
s.ID = id
if s.ExpiresAt.IsZero() {
s.ExpiresAt = time.Now().Add(365 * 24 * time.Hour)
if V(s.ExpiresAt).IsZero() {
s.ExpiresAt = P(time.Now().Add(365 * 24 * time.Hour))
}
firstId := strings.SplitN(s.ResourceIDs, ",", 2)[0]
@@ -125,10 +127,10 @@ 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() {
if !V(entity.(*model.Share).ExpiresAt).IsZero() {
cols = append(cols, "expires_at")
}
return r.Persistable.Update(id, entity, cols...)

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

@@ -4,6 +4,7 @@ import (
"github.com/google/wire"
"github.com/navidrome/navidrome/core/agents"
"github.com/navidrome/navidrome/core/ffmpeg"
"github.com/navidrome/navidrome/core/playback"
"github.com/navidrome/navidrome/core/scrobbler"
)
@@ -18,4 +19,5 @@ var Set = wire.NewSet(
agents.New,
ffmpeg.New,
scrobbler.GetPlayTracker,
playback.GetInstance,
)

View File

@@ -2,14 +2,15 @@ package db
import (
"database/sql"
"embed"
"fmt"
_ "github.com/mattn/go-sqlite3"
"github.com/navidrome/navidrome/conf"
_ "github.com/navidrome/navidrome/db/migration"
_ "github.com/navidrome/navidrome/db/migrations"
"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 migrations/*.sql
var embedMigrations embed.FS
const migrationsFolder = "migrations"
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() func() {
db := Db()
// Disable foreign_keys to allow re-creating tables in migrations
@@ -54,19 +60,53 @@ 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, "./")
if !isSchemaEmpty(db) && hasPendingMigrations(db, migrationsFolder) {
log.Info("Upgrading DB Schema to latest version")
}
goose.SetLogger(gooseLogger)
err = goose.Up(db, migrationsFolder)
if err != nil {
log.Fatal("Failed to apply new migrations", err)
}
return func() {
if err := Close(); err != nil {
log.Error("Error closing DB", err)
}
}
}
func isSchemaEmpty(db *sql.DB) bool { // nolint:interfacer
type statusLogger struct{ numPending int }
func (*statusLogger) Fatalf(format string, v ...interface{}) { log.Fatal(fmt.Sprintf(format, v...)) }
func (l *statusLogger) Printf(format string, v ...interface{}) {
if len(v) < 1 {
return
}
if v0, ok := v[0].(string); !ok {
return
} else if v0 == "Pending" {
l.numPending++
}
}
func hasPendingMigrations(db *sql.DB, folder string) bool {
l := &statusLogger{}
goose.SetLogger(l)
err := goose.Status(db, folder)
if err != nil {
log.Fatal("Failed to check for pending migrations", err)
}
return l.numPending > 0
}
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

@@ -1,20 +0,0 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20200310171621, Down20200310171621)
}
func Up20200310171621(tx *sql.Tx) error {
notice(tx, "A full rescan will be performed to enable search by Album Artist!")
return forceFullRescan(tx)
}
func Down20200310171621(tx *sql.Tx) error {
return nil
}

View File

@@ -1,20 +0,0 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20200326090707, Down20200326090707)
}
func Up20200326090707(tx *sql.Tx) error {
notice(tx, "A full rescan will be performed!")
return forceFullRescan(tx)
}
func Down20200326090707(tx *sql.Tx) error {
return nil
}

View File

@@ -1,20 +0,0 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20200409002249, Down20200409002249)
}
func Up20200409002249(tx *sql.Tx) error {
notice(tx, "A full rescan will be performed to enable search by individual Artist in an Album!")
return forceFullRescan(tx)
}
func Down20200409002249(tx *sql.Tx) error {
return nil
}

View File

@@ -1,20 +0,0 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20200418110522, Down20200418110522)
}
func Up20200418110522(tx *sql.Tx) error {
notice(tx, "A full rescan will be performed to fix search Albums by year")
return forceFullRescan(tx)
}
func Down20200418110522(tx *sql.Tx) error {
return nil
}

View File

@@ -1,20 +0,0 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20200419222708, Down20200419222708)
}
func Up20200419222708(tx *sql.Tx) error {
notice(tx, "A full rescan will be performed to change the search behaviour")
return forceFullRescan(tx)
}
func Down20200419222708(tx *sql.Tx) error {
return nil
}

View File

@@ -1,23 +0,0 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20201003111749, Down20201003111749)
}
func Up20201003111749(tx *sql.Tx) error {
_, err := tx.Exec(`
create index if not exists annotation_starred_at
on annotation (starred_at);
`)
return err
}
func Down20201003111749(tx *sql.Tx) error {
return nil
}

View File

@@ -1,23 +0,0 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20201021135455, Down20201021135455)
}
func Up20201021135455(tx *sql.Tx) error {
_, err := tx.Exec(`
create index if not exists media_file_artist_id
on media_file (artist_id);
`)
return err
}
func Down20201021135455(tx *sql.Tx) error {
return nil
}

View File

@@ -1,23 +0,0 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20201128100726, Down20201128100726)
}
func Up20201128100726(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table player
add report_real_path bool default FALSE not null;
`)
return err
}
func Down20201128100726(tx *sql.Tx) error {
return nil
}

View File

@@ -1,23 +0,0 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(upAddAlphabeticalByArtistIndex, downAddAlphabeticalByArtistIndex)
}
func upAddAlphabeticalByArtistIndex(tx *sql.Tx) error {
_, err := tx.Exec(`
create index album_alphabetical_by_artist
ON album(compilation, order_album_artist_name, order_album_name)
`)
return err
}
func downAddAlphabeticalByArtistIndex(tx *sql.Tx) error {
return nil
}

View File

@@ -1,22 +0,0 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(upRemoveInvalidArtistIds, downRemoveInvalidArtistIds)
}
func upRemoveInvalidArtistIds(tx *sql.Tx) error {
_, err := tx.Exec(`
update media_file set artist_id = '' where not exists(select 1 from artist where id = artist_id)
`)
return err
}
func downRemoveInvalidArtistIds(tx *sql.Tx) error {
return nil
}

View File

@@ -1,20 +0,0 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(upTouchPlaylists, downTouchPlaylists)
}
func upTouchPlaylists(tx *sql.Tx) error {
_, err := tx.Exec(`update playlist set updated_at = datetime('now');`)
return err
}
func downTouchPlaylists(tx *sql.Tx) error {
return nil
}

View File

@@ -1,17 +1,18 @@
package migrations
import (
"context"
"database/sql"
"github.com/navidrome/navidrome/log"
"github.com/pressly/goose"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200130083147, Down20200130083147)
goose.AddMigrationContext(Up20200130083147, Down20200130083147)
}
func Up20200130083147(tx *sql.Tx) error {
func Up20200130083147(_ context.Context, tx *sql.Tx) error {
log.Info("Creating DB Schema")
_, err := tx.Exec(`
create table if not exists album
@@ -178,6 +179,6 @@ create table if not exists user
return err
}
func Down20200130083147(tx *sql.Tx) error {
func Down20200130083147(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200131183653, Down20200131183653)
goose.AddMigrationContext(Up20200131183653, Down20200131183653)
}
func Up20200131183653(tx *sql.Tx) error {
func Up20200131183653(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table search_dg_tmp
(
@@ -36,7 +37,7 @@ update annotation set item_type = 'media_file' where item_type = 'mediaFile';
return err
}
func Down20200131183653(tx *sql.Tx) error {
func Down20200131183653(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table search_dg_tmp
(

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200208222418, Down20200208222418)
goose.AddMigrationContext(Up20200208222418, Down20200208222418)
}
func Up20200208222418(tx *sql.Tx) error {
func Up20200208222418(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
update annotation set play_count = 0 where play_count is null;
update annotation set rating = 0 where rating is null;
@@ -50,6 +51,6 @@ create index annotation_starred
return err
}
func Down20200208222418(tx *sql.Tx) error {
func Down20200208222418(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200220143731, Down20200220143731)
goose.AddMigrationContext(Up20200220143731, Down20200220143731)
}
func Up20200220143731(tx *sql.Tx) error {
func Up20200220143731(_ context.Context, tx *sql.Tx) error {
notice(tx, "This migration will force the next scan to be a full rescan!")
_, err := tx.Exec(`
create table media_file_dg_tmp
@@ -124,6 +125,6 @@ update media_file set updated_at = '0001-01-01';
return err
}
func Down20200220143731(tx *sql.Tx) error {
func Down20200220143731(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -0,0 +1,21 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigrationContext(Up20200310171621, Down20200310171621)
}
func Up20200310171621(_ context.Context, tx *sql.Tx) error {
notice(tx, "A full rescan will be performed to enable search by Album Artist!")
return forceFullRescan(tx)
}
func Down20200310171621(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200310181627, Down20200310181627)
goose.AddMigrationContext(Up20200310181627, Down20200310181627)
}
func Up20200310181627(tx *sql.Tx) error {
func Up20200310181627(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table transcoding
(
@@ -44,7 +45,7 @@ create table player
return err
}
func Down20200310181627(tx *sql.Tx) error {
func Down20200310181627(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
drop table transcoding;
drop table player;

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200319211049, Down20200319211049)
goose.AddMigrationContext(Up20200319211049, Down20200319211049)
}
func Up20200319211049(tx *sql.Tx) error {
func Up20200319211049(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table media_file
add full_text varchar(255) default '';
@@ -36,6 +37,6 @@ drop table if exists search;
return forceFullRescan(tx)
}
func Down20200319211049(tx *sql.Tx) error {
func Down20200319211049(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200325185135, Down20200325185135)
goose.AddMigrationContext(Up20200325185135, Down20200325185135)
}
func Up20200325185135(tx *sql.Tx) error {
func Up20200325185135(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table album
add album_artist_id varchar(255) default '';
@@ -29,6 +30,6 @@ create index media_file_artist_album_id
return forceFullRescan(tx)
}
func Down20200325185135(tx *sql.Tx) error {
func Down20200325185135(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -0,0 +1,21 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigrationContext(Up20200326090707, Down20200326090707)
}
func Up20200326090707(_ context.Context, tx *sql.Tx) error {
notice(tx, "A full rescan will be performed!")
return forceFullRescan(tx)
}
func Down20200326090707(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200327193744, Down20200327193744)
goose.AddMigrationContext(Up20200327193744, Down20200327193744)
}
func Up20200327193744(tx *sql.Tx) error {
func Up20200327193744(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table album_dg_tmp
(
@@ -75,6 +76,6 @@ create index album_max_year
return forceFullRescan(tx)
}
func Down20200327193744(tx *sql.Tx) error {
func Down20200327193744(_ context.Context, tx *sql.Tx) error {
return nil
}

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