Compare commits

...

117 Commits

Author SHA1 Message Date
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
242 changed files with 2888 additions and 1773 deletions

View File

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

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

@ -8,33 +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: Update ubuntu repo
run: sudo apt-get update
- uses: actions/checkout@v4
- name: Install taglib
run: sudo apt-get install libtag1-dev
- name: Set up Go 1.21
uses: actions/setup-go@v3
with:
go-version: 1.21.x
- 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
@ -47,26 +42,15 @@ jobs:
fi
go:
name: Test with Go ${{ matrix.go_version }}
name: Test Go code
runs-on: ubuntu-latest
strategy:
matrix:
go_version: [1.21.x, 1.20.x]
container: deluan/ci-goreleaser:1.22.3-1
steps:
- name: Update ubuntu repo
run: sudo apt-get update
- name: Install taglib
run: sudo apt-get install libtag1-dev ffmpeg
- 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'
@ -83,10 +67,10 @@ jobs:
env:
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: 18
node-version: 20
cache: "npm"
cache-dependency-path: "**/package-lock.json"
@ -110,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
@ -120,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.21.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.21.5-1
run: goreleaser release --clean --skip=publish --snapshot
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: goreleaser release --clean --skip=publish --snapshot
- name: Run GoReleaser - RELEASE
if: startsWith(github.ref, 'refs/tags/')
uses: docker://deluan/ci-goreleaser:1.21.5-1
run: goreleaser release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: goreleaser release --clean
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: binaries
path: |
@ -172,18 +149,18 @@ 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
@ -191,14 +168,14 @@ jobs:
- name: Login to Docker Hub
if: env.DOCKER_IMAGE != ''
uses: docker/login-action@v2
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@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
@ -207,7 +184,7 @@ jobs:
- name: Extract metadata for Docker
if: env.DOCKER_IMAGE != ''
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
labels: |
maintainer=deluan
@ -221,7 +198,7 @@ jobs:
- name: Build and Push
if: env.DOCKER_IMAGE != ''
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: .github/workflows/pipeline.dockerfile

View File

@ -8,7 +8,7 @@ jobs:
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 }}
@ -20,7 +20,7 @@ jobs:
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

View File

@ -1,6 +1,3 @@
run:
go: "1.20"
linters:
enable:
- asasalint
@ -28,8 +25,9 @@ linters:
- unused
- whitespace
issues:
exclude-rules:
- linters:
- gosec
text: "(G501|G401|G505):"
linters-settings:
gosec:
excludes:
- G501
- G401
- G505

2
.nvmrc
View File

@ -1 +1 @@
v18
v20

View File

@ -9,7 +9,7 @@ GIT_SHA=source_archive
GIT_TAG=$(patsubst navidrome-%,v%,$(notdir $(PWD)))
endif
CI_RELEASER_VERSION=1.21.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...
@ -61,12 +61,12 @@ snapshots: ##@Development Update (GoLang) Snapshot tests
migration-sql: ##@Development Create an empty SQL migration file
@if [ -z "${name}" ]; then echo "Usage: make migration-sql name=name_of_migration_file"; exit 1; fi
go run github.com/pressly/goose/v3/cmd/goose@latest -dir db/migration create ${name} sql
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/migration create ${name}
go run github.com/pressly/goose/v3/cmd/goose@latest -dir db/migrations create ${name}
.PHONY: migration
setup-dev: setup
@ -94,6 +94,7 @@ buildjs: check_node_env ##@Build Build only frontend
.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 --clean --skip=publish --snapshot
.PHONY: all
@ -105,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 --clean --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

View File

@ -7,7 +7,7 @@
[![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

View File

@ -2,31 +2,28 @@ 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"
"github.com/navidrome/navidrome/core/playback"
"github.com/navidrome/navidrome/db"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/resources"
"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
@ -42,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 {
@ -61,30 +62,42 @@ func preRun() {
conf.Load()
}
func runNavidrome() {
db.Init()
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 conf.Server.Jukebox.Enabled {
g.Go(startPlaybackServer(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)
@ -112,6 +125,7 @@ func startServer(ctx context.Context) func() error {
}
}
// 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
@ -141,22 +155,26 @@ 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 {
log.Info(ctx, "Starting playback server")
playbackInstance := playback.GetInstance()
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)
}
}
@ -192,6 +210,7 @@ func init() {
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`")

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:
@ -58,11 +58,13 @@ func CreateSubsonicAPIRouter() *subsonic.Router {
share := core.NewShare(dataStore)
archiver := core.NewArchiver(mediaStreamer, dataStore, share)
players := core.NewPlayers(dataStore)
scanner := GetScanner()
broker := events.GetBroker()
playlists := core.NewPlaylists(dataStore)
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
broker := events.GetBroker()
scannerScanner := scanner.GetInstance(dataStore, playlists, cacheWarmer, broker)
playTracker := scrobbler.GetPlayTracker(dataStore, broker)
router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, externalMetadata, scanner, broker, playlists, playTracker, share)
playbackServer := playback.GetInstance(dataStore)
router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, externalMetadata, scannerScanner, broker, playlists, playTracker, share, playbackServer)
return router
}
@ -96,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)
@ -107,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

@ -12,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"
)
@ -45,6 +44,7 @@ type configOptions struct {
EnableMediaFileCoverArt bool
TranscodingCacheSize string
ImageCacheSize string
AlbumPlayCountMode string
EnableArtworkPrecache bool
AutoImportPlaylists bool
PlaylistsPath string
@ -58,6 +58,7 @@ type configOptions struct {
SubsonicArtistParticipations bool
FFmpegPath string
MPVPath string
MPVCmdTemplate string
CoverArtPriority string
CoverJpegQuality int
ArtistArtPriority string
@ -79,6 +80,7 @@ type configOptions struct {
PasswordEncryptionKey string
ReverseProxyUserHeader string
ReverseProxyWhitelist string
HTTPSecurityHeaders secureOptions
Prometheus prometheusOptions
Scanner scannerOptions
Jukebox jukeboxOptions
@ -129,6 +131,10 @@ type listenBrainzOptions struct {
BaseURL string
}
type secureOptions struct {
CustomFrameOptionsValue string
}
type prometheusOptions struct {
Enabled bool
MetricsPath string
@ -137,9 +143,10 @@ type prometheusOptions struct {
type AudioDeviceDefinition []string
type jukeboxOptions struct {
Enabled bool
Devices []AudioDeviceDefinition
Default string
Enabled bool
Devices []AudioDeviceDefinition
Default string
AdminOnly bool
}
var (
@ -289,6 +296,7 @@ func init() {
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)
@ -304,6 +312,8 @@ func init() {
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")
@ -331,6 +341,7 @@ func init() {
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", ";/,")
@ -346,6 +357,8 @@ func init() {
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)
@ -358,7 +371,7 @@ func init() {
viper.SetDefault("devsidebarplaylists", true)
viper.SetDefault("devshowartistpage", true)
viper.SetDefault("devoffsetoptimize", 50000)
viper.SetDefault("devartworkmaxrequests", number.Max(2, runtime.NumCPU()/3))
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

@ -81,26 +81,36 @@ const (
DefaultCacheCleanUpInterval = 10 * time.Minute
)
const (
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 -ss %t -map 0:a: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 -ss %t -map 0:a: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 -ss %t -map 0:a: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,65 +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},
".tak": {typ: "audio/tak", 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

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

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

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

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

@ -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,7 @@ 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"
"golang.org/x/sync/errgroup"
)
@ -90,15 +89,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 +118,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 +202,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 +212,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 +243,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,
@ -272,7 +273,7 @@ func (e *externalMetadata) SimilarSongs(ctx context.Context, id string, count in
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)
@ -412,7 +413,7 @@ 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, compilation asc ",
Max: 1,

View File

@ -23,6 +23,7 @@ type FFmpeg interface {
Probe(ctx context.Context, files []string) (string, error)
CmdPath() (string, error)
IsAvailable() bool
Version() string
}
func New() FFmpeg {
@ -84,6 +85,24 @@ func (e *ffmpeg) IsAvailable() bool {
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}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"sync"
"github.com/navidrome/navidrome/core/playback/mpv"
"github.com/navidrome/navidrome/log"
@ -22,6 +23,7 @@ type Track interface {
}
type playbackDevice struct {
serviceCtx context.Context
ParentPlaybackServer PlaybackServer
Default bool
User string
@ -31,7 +33,7 @@ type playbackDevice struct {
Gain float32
PlaybackDone chan bool
ActiveTrack Track
TrackSwitcherStarted bool
startTrackSwitcher sync.Once
}
type DeviceStatus struct {
@ -43,8 +45,6 @@ type DeviceStatus struct {
const DefaultGain float32 = 1.0
var EmptyStatus = DeviceStatus{CurrentIndex: -1, Playing: false, Gain: DefaultGain, Position: 0}
func (pd *playbackDevice) getStatus() DeviceStatus {
pos := 0
if pd.ActiveTrack != nil {
@ -61,8 +61,9 @@ func (pd *playbackDevice) getStatus() DeviceStatus {
// 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(playbackServer PlaybackServer, name string, deviceName string) *playbackDevice {
func NewPlaybackDevice(ctx context.Context, playbackServer PlaybackServer, name string, deviceName string) *playbackDevice {
return &playbackDevice{
serviceCtx: ctx,
ParentPlaybackServer: playbackServer,
User: "",
Name: name,
@ -70,7 +71,6 @@ func NewPlaybackDevice(playbackServer PlaybackServer, name string, deviceName st
Gain: DefaultGain,
PlaybackQueue: NewQueue(),
PlaybackDone: make(chan bool),
TrackSwitcherStarted: false,
}
}
@ -103,14 +103,13 @@ func (pd *playbackDevice) Set(ctx context.Context, ids []string) (DeviceStatus,
func (pd *playbackDevice) Start(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, "Processing Start action", "device", pd)
if !pd.TrackSwitcherStarted {
pd.startTrackSwitcher.Do(func() {
log.Info(ctx, "Starting trackSwitcher goroutine")
// Start one trackSwitcher goroutine with each device
go func() {
pd.trackSwitcherGoroutine()
}()
pd.TrackSwitcherStarted = true
}
})
if pd.ActiveTrack != nil {
if pd.isPlaying() {
@ -255,23 +254,30 @@ func (pd *playbackDevice) isPlaying() bool {
func (pd *playbackDevice) trackSwitcherGoroutine() {
log.Debug("Started trackSwitcher goroutine", "device", pd)
for {
<-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)
select {
case <-pd.PlaybackDone:
log.Debug("Track switching detected")
if pd.ActiveTrack != nil {
pd.ActiveTrack.Close()
pd.ActiveTrack = nil
}
pd.ActiveTrack.Unpause()
} else {
log.Debug("There is no song left in the playlist. Finish.")
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
}
}
}
@ -283,10 +289,11 @@ func (pd *playbackDevice) switchActiveTrackByIndex(index int) error {
return errors.New("could not get current track")
}
track, err := mpv.NewTrack(pd.PlaybackDone, pd.DeviceName, *currentTrack)
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
}

View File

@ -2,14 +2,11 @@ package mpv
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
@ -17,16 +14,11 @@ import (
"github.com/navidrome/navidrome/log"
)
// mpv --no-audio-display --pause 'Jack Johnson/On And On/01 Times Like These.m4a' --input-ipc-server=/tmp/gonzo.socket
const (
mpvComdTemplate = "mpv --audio-device=%d --no-audio-display --pause %f --input-ipc-server=%s"
)
func start(args []string) (Executor, error) {
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()
err := j.start(ctx)
if err != nil {
return Executor{}, err
}
@ -46,12 +38,9 @@ type Executor struct {
out *io.PipeWriter
args []string
cmd *exec.Cmd
ctx context.Context
}
func (j *Executor) start() error {
ctx := context.Background()
j.ctx = ctx
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) {
@ -81,15 +70,14 @@ func (j *Executor) wait() {
}
// Path will always be an absolute path
func createMPVCommand(cmd, deviceName string, filename string, socketName string) []string {
split := strings.Split(fixCmd(cmd), " ")
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
}
@ -133,10 +121,3 @@ var (
mpvPath string
mpvErr error
)
func TempFileName(prefix, suffix string) string {
randBytes := make([]byte, 16)
// we can savely ignore the return value since we're loading into a precreated, fixedsized buffer
_, _ = rand.Read(randBytes)
return filepath.Join(os.TempDir(), prefix+hex.EncodeToString(randBytes)+suffix)
}

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
}

View File

@ -6,6 +6,7 @@ package mpv
// https://mpv.io/manual/master/#properties
import (
"context"
"fmt"
"os"
"time"
@ -24,17 +25,17 @@ type MpvTrack struct {
CloseCalled bool
}
func NewTrack(playbackDoneChannel chan bool, deviceName string, mf model.MediaFile) (*MpvTrack, error) {
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 := TempFileName("mpv-ctrl-", ".socket")
tmpSocketName := socketName("mpv-ctrl-", ".socket")
args := createMPVCommand(mpvComdTemplate, deviceName, mf.Path, tmpSocketName)
exe, err := start(args)
args := createMPVCommand(deviceName, mf.Path, tmpSocketName)
exe, err := start(ctx, args)
if err != nil {
log.Error("Error starting mpv process", err)
return nil, err
@ -110,24 +111,20 @@ func (t *MpvTrack) Close() {
log.Debug("sending shutdown command")
_, err := t.Conn.Call("quit")
if err != nil {
log.Error("Error sending quit command to mpv-ipc socket", err)
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.Error("Error canceling executor", err)
log.Warn("Error canceling executor", err)
}
}
}
}
if t.isSocketFilePresent() {
log.Debug("Removing socketfile", "socketfile", t.IPCSocketName)
err := os.Remove(t.IPCSocketName)
if err != nil {
log.Error("Error cleaning up socketfile", "socketfile", t.IPCSocketName, err)
}
removeSocket(t.IPCSocketName)
}
}

View File

@ -10,10 +10,8 @@ import (
"fmt"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/db"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/persistence"
"github.com/navidrome/navidrome/utils/singleton"
)
@ -21,7 +19,6 @@ type PlaybackServer interface {
Run(ctx context.Context) error
GetDeviceForUser(user string) (*playbackDevice, error)
GetMediaFile(id string) (*model.MediaFile, error)
GetCtx() *context.Context
}
type playbackServer struct {
@ -31,46 +28,41 @@ type playbackServer struct {
}
// GetInstance returns the playback-server singleton
func GetInstance() PlaybackServer {
func GetInstance(ds model.DataStore) PlaybackServer {
return singleton.GetInstance(func() *playbackServer {
return &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.datastore = persistence.New(db.Db())
devices, err := ps.initDeviceStatus(conf.Server.Jukebox.Devices, conf.Server.Jukebox.Default)
ps.playbackDevices = devices
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)
ps.ctx = &ctx
<-ctx.Done()
// Should confirm all subprocess are terminated before returning
return nil
}
// GetCtx produces the context this server was started with. Used for data-retrieval and cancellation
func (ps *playbackServer) GetCtx() *context.Context {
return ps.ctx
}
func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition, defaultDevice string) ([]playbackDevice, error) {
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 sythetic device named "auto"
// if there are no devices given and no default device, we create a synthetic device named "auto"
if len(devices) == 0 {
pbDevices[0] = *NewPlaybackDevice(ps, "auto", "auto")
pbDevices[0] = *NewPlaybackDevice(ctx, ps, "auto", "auto")
}
// if there is but only one entry and no default given, just use that.
@ -78,7 +70,7 @@ func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition,
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(ps, devices[0][0], devices[0][1])
pbDevices[0] = *NewPlaybackDevice(ctx, ps, devices[0][0], devices[0][1])
}
if len(devices) > 1 {
@ -94,7 +86,7 @@ func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition,
return []playbackDevice{}, fmt.Errorf("audio device definition ought to contain 2 fields, found: %d ", len(audioDevice))
}
pbDevices[idx] = *NewPlaybackDevice(ps, audioDevice[0], audioDevice[1])
pbDevices[idx] = *NewPlaybackDevice(ctx, ps, audioDevice[0], audioDevice[1])
if audioDevice[0] == defaultDevice {
pbDevices[idx].Default = true
@ -109,12 +101,12 @@ func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition,
}
func (ps *playbackServer) getDefaultDevice() (*playbackDevice, error) {
for idx, audioDevice := range ps.playbackDevices {
if audioDevice.Default {
for idx := range ps.playbackDevices {
if ps.playbackDevices[idx].Default {
return &ps.playbackDevices[idx], nil
}
}
return &playbackDevice{}, fmt.Errorf("no default device found")
return nil, fmt.Errorf("no default device found")
}
// GetMediaFile retrieves the MediaFile given by the id parameter
@ -128,7 +120,7 @@ func (ps *playbackServer) GetDeviceForUser(user string) (*playbackDevice, error)
// README: here we might plug-in the user-device mapping one fine day
device, err := ps.getDefaultDevice()
if err != nil {
return &playbackDevice{}, err
return nil, err
}
device.User = user
return device, nil

View File

@ -134,17 +134,3 @@ func (pd *Queue) IncreaseIndex() {
pd.SetIndex(pd.Index + 1)
}
}
func max(x, y int) int {
if x < y {
return y
}
return x
}
func min(x, y int) int {
if x > y {
return y
}
return x
}

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

View File

@ -5,6 +5,7 @@ import (
"os"
"time"
"github.com/navidrome/navidrome/model/criteria"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/model"
@ -33,29 +34,45 @@ var _ = Describe("Playlists", func() {
ps = NewPlaylists(ds)
})
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))
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))
})
})
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))
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{}))
})
})
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("ImportM3U", func() {

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

@ -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()) {
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]
@ -128,7 +130,7 @@ func (r *shareRepositoryWrapper) Update(id string, entity interface{}, _ ...stri
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

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

@ -7,7 +7,7 @@ import (
_ "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/v3"
@ -18,10 +18,10 @@ var (
Path string
)
//go:embed migration/*.sql
//go:embed migrations/*.sql
var embedMigrations embed.FS
const migrationsFolder = "migration"
const migrationsFolder = "migrations"
func Db() *sql.DB {
return singleton.GetInstance(func() *sql.DB {
@ -44,7 +44,7 @@ func Close() error {
return Db().Close()
}
func Init() {
func Init() func() {
db := Db()
// Disable foreign_keys to allow re-creating tables in migrations
@ -60,17 +60,50 @@ func Init() {
}
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)
}
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)
}
}
}
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 {

View File

@ -30,7 +30,7 @@ func upAddDefaultTranscodings(_ context.Context, tx *sql.Tx) error {
}
for _, t := range consts.DefaultTranscodings {
_, err := stmt.Exec(uuid.NewString(), t["name"], t["targetFormat"], t["defaultBitRate"], t["command"])
_, err := stmt.Exec(uuid.NewString(), t.Name, t.TargetFormat, t.DefaultBitRate, t.Command)
if err != nil {
return err
}

View File

@ -4,12 +4,12 @@ import (
"context"
"database/sql"
"path/filepath"
"slices"
"strings"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log"
"github.com/pressly/goose/v3"
"golang.org/x/exp/slices"
)
func init() {

View File

@ -4,12 +4,12 @@ import (
"context"
"database/sql"
"path/filepath"
"slices"
"strings"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log"
"github.com/pressly/goose/v3"
"golang.org/x/exp/slices"
)
func init() {

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