Compare commits

..

391 Commits

Author SHA1 Message Date
dependabot[bot]
910a46120b Bump dompurify from 2.4.5 to 2.5.6 in /ui (#3270)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.4.5 to 2.5.6.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.4.5...2.5.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 20:04:37 -04:00
dependabot[bot]
8c86d0945c Bump github.com/mileusna/useragent from 1.3.4 to 1.3.5 (#3269)
Bumps [github.com/mileusna/useragent](https://github.com/mileusna/useragent) from 1.3.4 to 1.3.5.
- [Release notes](https://github.com/mileusna/useragent/releases)
- [Commits](https://github.com/mileusna/useragent/compare/v1.3.4...v1.3.5)

---
updated-dependencies:
- dependency-name: github.com/mileusna/useragent
  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-09-16 19:13:29 -04:00
Caio Cotts
42047fde1a Remove shareURL value from config.js 2024-09-15 17:26:58 -04:00
Caio Cotts
2887cd65fc Fix wrong placement of When in test 2024-09-15 17:26:58 -04:00
Caio Cotts
8ac133027d Make the UI use the new ShareURL option 2024-09-15 17:26:58 -04:00
Caio Cotts
f0240280eb Add ShareURL configuration option 2024-09-15 17:26:58 -04:00
Reilly MacKenzie-Cree
d683688b0e Recursively refresh playlist tracks within smart playlist rules (#3018)
* Recursively refresh playlists within smart playlist rules

Signed-off-by: reillymc <reilly@mackenzie-cree.net>

* Clean up recursive smart playlist functions

Signed-off-by: reillymc <reilly@mackenzie-cree.net>

* Add smart playlist refresh timeout config and tests for nested track refetching

Signed-off-by: reillymc <reilly@mackenzie-cree.net>

* Change SmartPlaylistRefreshTimeout to SmartPlaylistRefreshDelay, increase default value

* Revert `smartPlaylistRefreshDelay` default to 5 seconds

---------

Signed-off-by: reillymc <reilly@mackenzie-cree.net>
Co-authored-by: Deluan <deluan@navidrome.org>
2024-09-15 13:27:54 -04:00
ChekeredList71
180035c1e3 Hungarian patch and typo fix for English (#3263)
* English typo fix

* hungarian-patch

You can find the changes here in detail: https://pastebin.com/GLtmwELv
2024-09-15 11:00:25 -04:00
Deluan
a132755d67 Move update-translations.sh script to workflow directory 2024-09-14 21:37:25 -04:00
Deluan
3107170afd Improve SQL sanitization 2024-09-14 18:53:34 -04:00
dependabot[bot]
d3bb4bb9a1 Bump send and express in /ui (#3260)
Bumps [send](https://github.com/pillarjs/send) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `send` from 0.18.0 to 0.19.0
- [Release notes](https://github.com/pillarjs/send/releases)
- [Changelog](https://github.com/pillarjs/send/blob/master/HISTORY.md)
- [Commits](https://github.com/pillarjs/send/compare/0.18.0...0.19.0)

Updates `express` from 4.20.0 to 4.21.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.20.0...4.21.0)

---
updated-dependencies:
- dependency-name: send
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-14 12:36:32 -04:00
dependabot[bot]
41f380451c Bump path-to-regexp and express in /ui (#3255)
Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `path-to-regexp` from 1.8.0 to 1.9.0
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v1.8.0...v1.9.0)

Updates `express` from 4.18.1 to 4.20.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.1...4.20.0)

---
updated-dependencies:
- dependency-name: path-to-regexp
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-14 12:30:29 -04:00
Deluan
e65eb225c8 Small refactoring
- Remove duplication
- Remove warning about builtin keyword `new`
2024-09-13 20:18:00 -04:00
Deluan
e8d0f2ec2c Allow searching songs by filepath, for songs without Title 2024-09-13 18:04:21 -04:00
Deluan
47872c9e8a Fix pipeline 2024-09-13 17:43:50 -04:00
Deluan
9ae2ec1a07 Ignore #snapshot folders when scanning. Fixes #3257 2024-09-13 17:30:08 -04:00
Deluan
a1866c7ff3 Fix log message 2024-09-13 09:13:51 -04:00
Kendall Garner
9f1794b97e Only refresh smart playlist when fetching first track (#3244)
* Only refresh smart playlist when fetching first track

* res -> w
2024-09-10 20:18:37 -04:00
dependabot[bot]
e1762882e3 Bump github.com/prometheus/client_golang from 1.20.2 to 1.20.3 (#3245)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.2 to 1.20.3.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/v1.20.3/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.20.2...v1.20.3)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  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-09-09 18:03:16 -04:00
dependabot[bot]
870b217eb9 Bump github.com/pressly/goose/v3 from 3.21.1 to 3.22.0 (#3247)
Bumps [github.com/pressly/goose/v3](https://github.com/pressly/goose) from 3.21.1 to 3.22.0.
- [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.21.1...v3.22.0)

---
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>
2024-09-09 17:59:30 -04:00
dependabot[bot]
53af567b45 Bump golang.org/x/image from 0.19.0 to 0.20.0 (#3248)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.19.0 to 0.20.0.
- [Commits](https://github.com/golang/image/compare/v0.19.0...v0.20.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-09-09 17:59:01 -04:00
dependabot[bot]
605aaf87d8 Bump github.com/mattn/go-sqlite3 from 1.14.22 to 1.14.23 (#3249)
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.22 to 1.14.23.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.22...v1.14.23)

---
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-09 17:58:50 -04:00
dependabot[bot]
9950538089 Bump github.com/mattn/go-zglob from 0.0.5 to 0.0.6 (#3231)
Bumps [github.com/mattn/go-zglob](https://github.com/mattn/go-zglob) from 0.0.5 to 0.0.6.
- [Commits](https://github.com/mattn/go-zglob/compare/v0.0.5...v0.0.6)

---
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>
2024-09-02 13:22:49 -04:00
Vlad Shulcz
4a55a148cf refactor(core): Refactor selectTranscodingOptions function (#3227)
* refactor(core): Refactor selectTranscodingOptions function - #3226

Signed-off-by: shulcz <vshulcz@gmail.com>

* chore: Fix selectTranscodingOptions function - #3226

Signed-off-by: shulcz <vshulcz@gmail.com>

* Small refactoring to make code more concise

* Fix log message

---------

Signed-off-by: shulcz <vshulcz@gmail.com>
Co-authored-by: Deluan <deluan@navidrome.org>
2024-09-02 12:20:23 -04:00
Deluan
c1b75bca51 Improve change detection for POEditor files 2024-09-02 11:02:24 -04:00
Reilly MacKenzie-Cree
5baab4af77 Update dev container to use Go 1.23 and customizations object (#3228)
Signed-off-by: reillymc <reilly@mackenzie-cree.net>
2024-09-01 22:22:32 -04:00
Xabi
4c87a39242 Add Basque localisation (#3221)
* Add Basque localisation

Initial Basque localisation

* Update eu.json

fixes extra dash

* Update eu.json

fixes

* Update eu.json

653098th time's the charm
2024-09-01 16:03:15 -04:00
Deluan
fc5d18feb7 Change error code type to avoid integer overflow conversion warning 2024-09-01 14:49:48 -04:00
Deluan
4612b0a518 Bump Go dependencies 2024-08-31 19:20:38 -04:00
Deluan Quintão
68ddbf4856 Add i18n lint job 2024-08-31 14:54:04 -04:00
dependabot[bot]
a6d72d8623 Bump webpack from 5.76.1 to 5.94.0 in /ui (#3218)
Bumps [webpack](https://github.com/webpack/webpack) from 5.76.1 to 5.94.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.76.1...v5.94.0)

---
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>
2024-08-29 16:11:25 -04:00
Deluan
1a41525a7e Upgrade go.mod to 1.23, allow override CI_RELEASER_VERSION for make single and make all 2024-08-29 15:14:20 -04:00
Deluan
8ca1aefad6 Change DefaultPlaylistPublicVisibility to false 2024-08-28 19:23:19 -04:00
John White
67d11dd144 feat: imported playlists are public by default (#3143)
* feat: imported playlists are public by default

* chore: make linter happy

---------

Co-authored-by: John White <john@activecode.dev>
2024-08-28 19:20:05 -04:00
Deluan Quintão
9f65f8f5a8 Update translations (#3164)
Co-authored-by: deluan <331353+deluan@users.noreply.github.com>
2024-08-28 19:14:27 -04:00
Deluan Quintão
bc06a59919 Upgrade TagLib 2.0.2, GoReleaser 2.2.0 (#3217)
* Upgrade ci-goreleaser

* Fix tests

* Fix taglib lib path in macOS
2024-08-28 19:13:08 -04:00
Sunny
6709ab3c5e fix(common): Hide Share/Get Info items in disc context menu - #3204 (#3209)
Signed-off-by: Sunny <sunny@sny.sh>
2024-08-26 21:40:05 -04:00
dependabot[bot]
195f2b3f38 Bump @testing-library/jest-dom from 6.4.8 to 6.5.0 in /ui (#3216)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 6.4.8 to 6.5.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/v6.4.8...v6.5.0)

---
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-08-26 21:30:56 -04:00
dependabot[bot]
6ea688e720 Bump github.com/prometheus/client_golang from 1.20.0 to 1.20.2 (#3213)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.0 to 1.20.2.
- [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.20.0...v1.20.2)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  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-08-26 21:30:44 -04:00
dependabot[bot]
496c95fd47 Bump github.com/go-chi/httprate from 0.12.1 to 0.14.0 (#3211)
Bumps [github.com/go-chi/httprate](https://github.com/go-chi/httprate) from 0.12.1 to 0.14.0.
- [Release notes](https://github.com/go-chi/httprate/releases)
- [Commits](https://github.com/go-chi/httprate/compare/v0.12.1...v0.14.0)

---
updated-dependencies:
- dependency-name: github.com/go-chi/httprate
  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-08-26 21:30:31 -04:00
dependabot[bot]
108bf31148 Bump github.com/pelletier/go-toml/v2 from 2.2.2 to 2.2.3 (#3212)
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.2.2 to 2.2.3.
- [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.2...v2.2.3)

---
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-08-26 21:30:14 -04:00
dependabot[bot]
7c81143ca9 Bump github.com/onsi/ginkgo/v2 from 2.20.0 to 2.20.1 (#3215)
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.20.0 to 2.20.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.20.0...v2.20.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>
2024-08-26 21:29:39 -04:00
dependabot[bot]
533c394f09 Bump github.com/jellydator/ttlcache/v3 from 3.2.0 to 3.2.1 (#3214)
Bumps [github.com/jellydator/ttlcache/v3](https://github.com/jellydator/ttlcache) from 3.2.0 to 3.2.1.
- [Release notes](https://github.com/jellydator/ttlcache/releases)
- [Commits](https://github.com/jellydator/ttlcache/compare/v3.2.0...v3.2.1)

---
updated-dependencies:
- dependency-name: github.com/jellydator/ttlcache/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>
2024-08-26 21:29:28 -04:00
Deluan
c95fa11a2f Remove potential integer overflow conversion uint64 -> int64 2024-08-22 19:28:22 -04:00
Deluan
5d81849603 Fix lint errors 2024-08-21 12:15:25 -04:00
dependabot[bot]
1a8bef0743 Bump react-icons from 5.2.1 to 5.3.0 in /ui (#3200)
Bumps [react-icons](https://github.com/react-icons/react-icons) from 5.2.1 to 5.3.0.
- [Release notes](https://github.com/react-icons/react-icons/releases)
- [Commits](https://github.com/react-icons/react-icons/compare/v5.2.1...v5.3.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-08-19 18:21:10 -04:00
dependabot[bot]
85bf7b5684 Bump @testing-library/jest-dom from 6.4.6 to 6.4.8 in /ui (#3172)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 6.4.6 to 6.4.8.
- [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.4.6...v6.4.8)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  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-08-19 18:13:38 -04:00
dependabot[bot]
bdbff1ea38 Bump prettier from 3.3.2 to 3.3.3 in /ui (#3171)
Bumps [prettier](https://github.com/prettier/prettier) from 3.3.2 to 3.3.3.
- [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.3.2...3.3.3)

---
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-08-19 18:12:45 -04:00
dependabot[bot]
5d58048780 Bump github.com/prometheus/client_golang from 1.19.1 to 1.20.0 (#3199)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.19.1 to 1.20.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.19.1...v1.20.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-08-19 18:12:29 -04:00
Rob Emery
723f01d98c Fixing Build/lint error: "non-constant format string in call to fmt.Errorf (govet)" (#3198)
* Fixing " non-constant format string in call to fmt.Errorf (govet)"

* Its a string, not an int; read better.
2024-08-19 17:58:35 -04:00
Deluan Quintão
c4bd0e67fa Upgrade Go to 1.23 (#3190)
* Upgrade to Golang 1.23rc1

* Fix imports

* Go 1.23 final version

* Fix lint compatibility with ci-goreleaser
2024-08-19 17:47:54 -04:00
Deluan
0c33523f45 Bump dependencies 2024-08-10 12:22:36 -04:00
Deluan
14d085f651 Deprecate buildall 2024-08-07 16:19:44 -04:00
Deluan
4d4c71212f Build UI bundle on demand 2024-08-07 15:36:29 -04:00
Deluan
e1ba152a38 Reduce noise in logs when pre-caching artwork 2024-08-07 13:08:54 -04:00
Deluan
eaa7f7c7e9 Fix Player filter 2024-08-05 18:21:21 -04:00
Kendall Garner
290333ec59 Use same key for replaygain's preAmp (#3184)
Resolves #2933. To prevent this from happening again, make the localstorage keys consts for set/get
2024-08-03 21:18:41 -04:00
Kendall Garner
fa85e2a781 Use userId in player, other fixes (#3182)
* [bugfix] player: use userId, other fixes

This PR primarily resolves #1928 by switching the foreign key of `player` from `user.user_name` to `user.id`.
There are also a few other fixes/changes:

- For some bizarre reason, `ip_address` is never returned from `read`/`get`. Change the field to `ip`, which works. Somehow
- Update `players_test.go` mock to also check for user agent, replicating the actual code
- Update `player_repository.go` `isPermitted` to check user id. I don't know how this worked before...
- tests!
- a few places referred to `typ`, when it is really `userAgent`. Change the field names

* baseRequest -> selectPlayer

* remove comment

* update migration, make all of persistence foreign key enabled

* maybe don't forget to save the file first
2024-08-03 13:37:21 -04:00
dependabot[bot]
5360283bb0 Bump github.com/onsi/gomega from 1.33.1 to 1.34.0 (#3176)
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.33.1 to 1.34.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.33.1...v1.34.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-07-31 21:35:00 -04:00
dependabot[bot]
e59d81bf78 Bump github.com/microcosm-cc/bluemonday from 1.0.26 to 1.0.27 (#3141)
Bumps [github.com/microcosm-cc/bluemonday](https://github.com/microcosm-cc/bluemonday) from 1.0.26 to 1.0.27.
- [Release notes](https://github.com/microcosm-cc/bluemonday/releases)
- [Commits](https://github.com/microcosm-cc/bluemonday/compare/v1.0.26...v1.0.27)

---
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>
2024-07-28 15:33:40 -04:00
Deluan
7b2ddfd65a Fix "Cannot read properties of undefined". Closes #3070 2024-07-25 17:22:04 -04:00
Deluan
76c3f5131a Use SHA256 in Gravatar URLs 2024-07-23 17:49:46 -04:00
Soderes
f577704d7a Add Hungarian language (#3157) 2024-07-22 18:10:41 -04:00
dependabot[bot]
f46ff73c53 Bump github.com/go-chi/httprate from 0.9.0 to 0.10.0 (#3160)
Bumps [github.com/go-chi/httprate](https://github.com/go-chi/httprate) from 0.9.0 to 0.10.0.
- [Commits](https://github.com/go-chi/httprate/compare/v0.9.0...v0.10.0)

---
updated-dependencies:
- dependency-name: github.com/go-chi/httprate
  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-07-22 15:31:46 -04:00
Deluan
d046c180bf Fix race condition 2024-07-22 14:27:02 -04:00
Caio Cotts
9b4abd9e5a Add Auto-Import toggle switch to playlists list view. 2024-07-18 00:07:59 +02:00
Caio Cotts
0de5f594fe Remove unnecessary Fragment component. 2024-07-18 00:07:59 +02:00
Deluan
33717f26d4 Fix album sorting in Artist page 2024-07-04 17:21:31 -04:00
dependabot[bot]
6722395879 Bump github.com/unrolled/secure from 1.14.0 to 1.15.0 (#3127)
Bumps [github.com/unrolled/secure](https://github.com/unrolled/secure) from 1.14.0 to 1.15.0.
- [Release notes](https://github.com/unrolled/secure/releases)
- [Commits](https://github.com/unrolled/secure/compare/v1.14.0...v1.15.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-07-01 15:53:01 -04:00
dependabot[bot]
2667ad3921 Bump github.com/go-chi/chi/v5 from 5.0.14 to 5.1.0 (#3126)
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.14 to 5.1.0.
- [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.14...v5.1.0)

---
updated-dependencies:
- dependency-name: github.com/go-chi/chi/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>
2024-07-01 15:52:29 -04:00
Kendall Garner
3e1fa20413 fix background color for nord theme (#3124) 2024-06-29 18:50:33 -04:00
gruneforth
1802015737 Add Nuclear Theme (#3098) 2024-06-29 17:04:30 -04:00
Deluan
47378c6882 Remove unnecessary annotation table primary key 2024-06-29 11:45:41 -04:00
dependabot[bot]
81459cc421 Bump github.com/spf13/cobra from 1.8.0 to 1.8.1 (#3095)
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  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-06-26 17:47:24 -04:00
dependabot[bot]
4cda3a58dc Bump braces from 3.0.2 to 3.0.3 in /ui (#3085)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-26 17:47:00 -04:00
dependabot[bot]
56557bb0f3 Bump @testing-library/jest-dom from 6.4.5 to 6.4.6 in /ui (#3096)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 6.4.5 to 6.4.6.
- [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.4.5...v6.4.6)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  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-06-26 17:46:44 -04:00
dependabot[bot]
c60f443179 Bump prettier from 3.3.1 to 3.3.2 in /ui (#3097)
Bumps [prettier](https://github.com/prettier/prettier) from 3.3.1 to 3.3.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.3.1...3.3.2)

---
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-06-26 17:46:34 -04:00
dependabot[bot]
fa3998d6e1 Bump github.com/pressly/goose/v3 from 3.20.0 to 3.21.1 (#3114)
Bumps [github.com/pressly/goose/v3](https://github.com/pressly/goose) from 3.20.0 to 3.21.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.20.0...v3.21.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>
2024-06-26 17:45:44 -04:00
dependabot[bot]
8542ac96c0 Bump github.com/go-chi/chi/v5 from 5.0.12 to 5.0.14 (#3115)
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.12 to 5.0.14.
- [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.12...v5.0.14)

---
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-06-26 17:45:25 -04:00
dependabot[bot]
4557add7ef Bump github.com/lestrrat-go/jwx/v2 from 2.0.21 to 2.1.0 (#3113)
Bumps [github.com/lestrrat-go/jwx/v2](https://github.com/lestrrat-go/jwx) from 2.0.21 to 2.1.0.
- [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.21...v2.1.0)

---
updated-dependencies:
- dependency-name: github.com/lestrrat-go/jwx/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-06-26 17:45:13 -04:00
dependabot[bot]
004fae43f5 Bump golang.org/x/image from 0.17.0 to 0.18.0 (#3119)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.17.0 to 0.18.0.
- [Commits](https://github.com/golang/image/compare/v0.17.0...v0.18.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-26 17:44:58 -04:00
Deluan
7111535963 Don't panic on PostScan errors. Fix #3118 2024-06-25 17:14:17 -04:00
Deluan
3bc9e75b28 Evict expired items from SimpleCache 2024-06-24 17:32:34 -04:00
Deluan
3993c4d17f Upgrade to ttlcache/v3 2024-06-21 18:09:34 -04:00
Deluan
29b7b740ce Also use SimpleCache in cache.HTTPClient 2024-06-21 17:40:18 -04:00
Deluan
29bc17acd7 Wrap ttlcache in our own SimpleCache implementation 2024-06-21 17:21:09 -04:00
Deluan
4044642abf Add http headers to trace log 2024-06-16 22:31:47 -04:00
Kendall Garner
88eac6d7f3 fix album/media file random sort (#3089) 2024-06-12 21:06:59 -04:00
dependabot[bot]
f267f55713 Bump github.com/prometheus/client_golang from 1.19.0 to 1.19.1
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.19.0 to 1.19.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.19.0...v1.19.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 22:56:03 -04:00
dependabot[bot]
58990c4830 Bump @testing-library/jest-dom from 6.4.2 to 6.4.5 in /ui
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 6.4.2 to 6.4.5.
- [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.4.2...v6.4.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 22:50:38 -04:00
dependabot[bot]
7a20233a35 Bump ejs from 3.1.9 to 3.1.10 in /ui
Bumps [ejs](https://github.com/mde/ejs) from 3.1.9 to 3.1.10.
- [Release notes](https://github.com/mde/ejs/releases)
- [Commits](https://github.com/mde/ejs/compare/v3.1.9...v3.1.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 22:48:44 -04:00
dependabot[bot]
45679e11c2 Bump clsx from 2.1.0 to 2.1.1 in /ui
Bumps [clsx](https://github.com/lukeed/clsx) from 2.1.0 to 2.1.1.
- [Release notes](https://github.com/lukeed/clsx/releases)
- [Commits](https://github.com/lukeed/clsx/compare/v2.1.0...v2.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 22:42:52 -04:00
dependabot[bot]
05f34b0cce Bump golang.org/x/image from 0.16.0 to 0.17.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.16.0 to 0.17.0.
- [Commits](https://github.com/golang/image/compare/v0.16.0...v0.17.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>
2024-06-10 22:35:02 -04:00
dependabot[bot]
586e725d6c Bump react-icons from 5.1.0 to 5.2.1 in /ui
Bumps [react-icons](https://github.com/react-icons/react-icons) from 5.1.0 to 5.2.1.
- [Release notes](https://github.com/react-icons/react-icons/releases)
- [Commits](https://github.com/react-icons/react-icons/compare/v5.1.0...v5.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 22:27:47 -04:00
dependabot[bot]
a7c4c72dc6 Bump uuid from 9.0.1 to 10.0.0 in /ui
Bumps [uuid](https://github.com/uuidjs/uuid) from 9.0.1 to 10.0.0.
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v9.0.1...v10.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 22:17:49 -04:00
Deluan
232c45bd06 Increase artist image url sizes.
See https://support.symfonium.app/t/artist-picture-less-compressed/4447
2024-06-10 16:33:41 -04:00
Caio Cotts
1b77830eb4 Do not use lastFM api key and secret to determine if LastFM.Enabled should be set. 2024-06-10 16:26:39 -04:00
dependabot[bot]
e535f7eb78 Bump prettier from 3.3.0 to 3.3.1 in /ui
Bumps [prettier](https://github.com/prettier/prettier) from 3.3.0 to 3.3.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/3.3.0...3.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 15:31:43 -04:00
Deluan
d8b2f3d2cf Don't expose fullText data in the Native API 2024-06-09 11:19:22 -04:00
kartikynwa
56303cde23 Add R128_{TRACK,ALBUM}_GAIN support to the scanner (#3072)
* Add R128 gain tags support to the scanner

* Add R128 test to metadata_internal_test.go

* Pass explicit tag names to getGainValue function
2024-06-08 13:45:06 -04:00
Deluan
e434ca9372 Change resized image cache key 2024-06-08 13:37:30 -04:00
Deluan
3252fab171 Increase artist image url sizes.
See https://support.symfonium.app/t/artist-picture-less-compressed/4447
2024-06-08 13:32:57 -04:00
Deluan
6d526870b7 Fix race condition in external metadata retrieval 2024-06-06 21:01:35 -04:00
Deluan
34678611c0 Small refactoring 2024-06-06 20:15:34 -04:00
Deluan
0f7d6b5bc4 More micro-optimizations 2024-06-06 07:11:43 -04:00
Deluan
939f3eee97 Initialize Index Groups regex just once 2024-06-05 23:00:36 -04:00
Deluan
b4ef1b1e38 Replace gg.If with cmp.Or 2024-06-05 22:48:00 -04:00
Deluan
11bef060a3 Small refactoring 2024-06-05 22:40:22 -04:00
Deluan
abe5690018 Refactor string utilities into its own package str 2024-06-05 22:09:27 -04:00
Deluan
46fc38bf61 Fix tests expectations 2024-06-05 19:54:25 -04:00
dependabot[bot]
6d8d519807 Bump prettier from 3.2.5 to 3.3.0 in /ui (#3069)
Bumps [prettier](https://github.com/prettier/prettier) from 3.2.5 to 3.3.0.
- [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.5...3.3.0)

---
updated-dependencies:
- dependency-name: prettier
  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-06-03 14:02:49 -04:00
dependabot[bot]
da9cf22b6b Bump github.com/spf13/viper from 1.18.2 to 1.19.0 (#3068)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.18.2 to 1.19.0.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.18.2...v1.19.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/viper
  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-06-03 13:25:33 -04:00
Deluan
8c3919d6a0 Simplify dbx wrapper 2024-06-01 15:01:28 -04:00
dependabot[bot]
4df69bd334 Bump github.com/onsi/ginkgo/v2 from 2.17.3 to 2.19.0 (#3054)
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.17.3 to 2.19.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.17.3...v2.19.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>
2024-05-27 14:10:56 -04:00
Deluan
ee73a9d297 Small optimization in MediaFiles.ToAlbum() 2024-05-26 14:28:23 -04:00
Caio Cotts
0488fb92cb Fix image stuttering (#3035)
* Fix image stuttering.

* Fix docker publishing for PRs

* Write tests for new square parameter.

* Simplify code for createImage.

---------

Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2024-05-24 20:19:26 -04:00
Deluan
61903facdf Revert isDBInitialized 2024-05-22 16:20:57 -04:00
Drew Weymouth
b6fce0e686 Fix XML marshaling of OpenSubsonic structured lyrics (#3041) 2024-05-22 12:15:14 -04:00
Deluan
f88d3f82da Replace panics with log.Fatals 2024-05-21 17:50:02 -04:00
Deluan
55bff343cd Optimize SQLite3 access. Mainly separate read access from write access.
Based on tips from https://archive.is/Xfjh6#selection-257.0-278.0
2024-05-21 17:19:41 -04:00
dependabot[bot]
68f03d0167 Bump github.com/matoous/go-nanoid/v2 from 2.0.0 to 2.1.0 (#3038)
Bumps [github.com/matoous/go-nanoid/v2](https://github.com/matoous/go-nanoid) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/matoous/go-nanoid/releases)
- [Commits](https://github.com/matoous/go-nanoid/compare/v2.0.0...v2.1.0)

---
updated-dependencies:
- dependency-name: github.com/matoous/go-nanoid/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-05-21 10:45:04 -04:00
Deluan
643c763cb4 Show number of results from a query in the logs 2024-05-20 16:21:41 -04:00
Deluan Quintão
67865512c8 Update Ukrainian translations (#3029)
Co-authored-by: deluan <331353+deluan@users.noreply.github.com>
2024-05-19 22:17:13 -04:00
Deluan
b2ecc1d16f Fix G404 gosec lint error 2024-05-19 21:55:19 -04:00
Deluan
bcaa180fc7 Fix 32 bits builds 2024-05-19 13:03:13 -04:00
Deluan
aeed5a7099 Update caniuse-lite 2024-05-19 12:45:19 -04:00
Deluan
3977ef6e0f Make first WebUI random page stick 2024-05-19 12:35:30 -04:00
Deluan
653b4d97f9 Add missing Test function 2024-05-18 15:05:40 -04:00
Guilherme Souza
98218d045e Deterministic pagination in random albums sort (#1841)
* Deterministic pagination in random albums sort

* Reseed on first random page

* Add unit tests

* Use rand in Subsonic API

* Use different seeds per user on SEEDEDRAND() SQLite3 function

* Small refactor

* Fix id mismatch

* Add seeded random to media_file (subsonic endpoint `getRandomSongs`)

* Refactor

* Remove unneeded import

---------

Co-authored-by: Deluan <deluan@navidrome.org>
2024-05-18 14:10:53 -04:00
Deluan
a9feeac793 Revert "Always run docker steps (#3034)"
This reverts commit 5d41165b5b.
2024-05-18 11:54:16 -04:00
Deluan
1c0551f4f7 Revert "Fix docker publishing for PRs"
This reverts commit 15c9a0ded3.
2024-05-18 11:54:15 -04:00
Deluan
15c9a0ded3 Fix docker publishing for PRs 2024-05-17 22:45:54 -04:00
Deluan Quintão
5d41165b5b Always run docker steps (#3034) 2024-05-17 22:22:47 -04:00
Deluan
0a763b91d5 Fix lint error 2024-05-17 21:46:59 -04:00
Deluan
4d28d534cc Refactor random.WeightedChooser, unsing generics 2024-05-17 15:45:34 -04:00
Deluan
a7a4fb522c Simplify resources.FS 2024-05-16 22:53:51 -04:00
Deluan
7f52ff72dc Simplify image format detection code 2024-05-16 13:49:40 -04:00
Deluan
8ed07333ed Improve resizeImage code readability 2024-05-16 13:49:40 -04:00
Rob Emery
52235c291d Fix memory leak in CachedGenreRepository (#3031)
that the scanner was run, the ttlcache was also created each time.
This caused (under testing with 166 genres in the database) the
memory consumed by navidrome to 101.18MB over approx 3 days; 96%
of which is in instances of this cache. Swapping to a singleton
has reduced this to down to ~ 2.6MB

Co-authored-by: Rob Emery <git@mintsoft.net>
2024-05-16 12:16:56 -04:00
Fynn Petersen-Frey
de0a08915c fix bug in jukebox: property unavailable (#3024)
* fix bug in jukebox: property unavailable

* fix lint error
2024-05-15 09:48:09 -04:00
Deluan
45c4583f1b Fix race condition 2024-05-13 09:28:19 -04:00
Deluan
478c709a64 Associate main entities with library 2024-05-12 21:37:42 -04:00
Deluan
477bcaee58 Store MusicFolder as a library in DB 2024-05-12 21:37:42 -04:00
Deluan
081ef85db6 Rename MediaFolder to Library 2024-05-12 21:37:42 -04:00
Deluan
6f2643e55e Refactor to use more Go 1.22 features 2024-05-12 20:04:53 -04:00
Deluan
9ee63b39cb Update Go to 1.22.3 2024-05-12 20:04:53 -04:00
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
499 changed files with 16175 additions and 10606 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.23",
// Options
"INSTALL_NODE": "true",
"NODE_VERSION": "v18"
"NODE_VERSION": "v20"
}
},
"workspaceMount": "",
@@ -18,33 +18,37 @@
"--volume=${localWorkspaceFolder}:/workspaces/${localWorkspaceFolderBasename}:Z"
],
// Set *default* container specific settings.json values on container create.
"settings": {
"terminal.integrated.shell.linux": "/bin/bash",
"go.useGoProxyToCheckForToolUpdates": false,
"go.useLanguageServer": true,
"go.gopath": "/go",
"go.goroot": "/usr/local/go",
"go.toolsGopath": "/go/bin",
"go.formatTool": "goimports",
"go.lintOnSave": "package",
"go.lintTool": "golangci-lint",
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
"customizations": {
"vscode": {
"settings": {
"terminal.integrated.shell.linux": "/bin/bash",
"go.useGoProxyToCheckForToolUpdates": false,
"go.useLanguageServer": true,
"go.gopath": "/go",
"go.goroot": "/usr/local/go",
"go.toolsGopath": "/go/bin",
"go.formatTool": "goimports",
"go.lintOnSave": "package",
"go.lintTool": "golangci-lint",
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
}
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"golang.Go",
"esbenp.prettier-vscode",
"tamasfe.even-better-toml"
]
}
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"golang.Go",
"esbenp.prettier-vscode",
"tamasfe.even-better-toml"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
4533,

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: Test, Lint, Build'
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.23.0-1
steps:
- name: Install taglib
run: sudo apt-get install libtag1-dev
- uses: actions/checkout@v4
- 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
@@ -44,23 +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.23.0-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'
@@ -75,14 +65,14 @@ jobs:
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: 18
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,51 +94,60 @@ jobs:
cd ui
npm run build
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: js-bundle
path: ui/build
retention-days: 7
i18n-lint:
name: Lint i18n files
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
set -e
for file in resources/i18n/*.json; do
echo "Validating $file"
if ! jq empty "$file" 2>error.log; then
error_message=$(cat error.log)
line_number=$(echo "$error_message" | grep -oP 'line \K[0-9]+')
echo "::error file=$file,line=$line_number::$error_message"
exit 1
fi
done
binaries:
name: Build binaries
needs: [js, go, go-lint]
needs: [js, go, go-lint, i18n-lint]
runs-on: ubuntu-latest
container: deluan/ci-goreleaser:1.23.0-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.0-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.0-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.21.0-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: |
@@ -166,18 +165,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
@@ -185,14 +184,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 }}
@@ -201,7 +200,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
@@ -215,7 +214,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

@@ -12,8 +12,9 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v4.0.0
- uses: dessant/lock-threads@v5
with:
process-only: 'issues, prs'
issue-inactive-days: 120
pr-inactive-days: 120
log-output: true
@@ -27,7 +28,7 @@ jobs:
This pull request has been automatically locked since there
has not been any recent activity after it was closed.
Please open a new issue for related bugs.
- uses: actions/stale@v7
- uses: actions/stale@v9
with:
operations-per-run: 999
days-before-issue-stale: 180

44
.github/workflows/update-translations.sh vendored Executable file
View File

@@ -0,0 +1,44 @@
#!/bin/sh
set -e
I18N_DIR=resources/i18n
# Function to process JSON: remove empty attributes and sort
process_json() {
jq 'walk(if type == "object" then with_entries(select(.value != null and .value != "" and .value != [] and .value != {})) | to_entries | sort_by(.key) | from_entries else . end)' "$1"
}
check_lang_diff() {
filename=${I18N_DIR}/"$1".json
url=$(curl -s -X POST https://poeditor.com/api/ \
-d api_token="${POEDITOR_APIKEY}" \
-d action="export" \
-d id="${POEDITOR_PROJECTID}" \
-d language="$1" \
-d type="key_value_json" | jq -r .item)
if [ -z "$url" ]; then
echo "Failed to export $1"
return 1
fi
curl -sSL "$url" > poeditor.json
process_json "$filename" > "$filename".tmp
process_json poeditor.json > poeditor.tmp
diff=$(diff -u "$filename".tmp poeditor.tmp) || true
if [ -n "$diff" ]; then
echo "$diff"
mv poeditor.json "$filename"
fi
rm -f poeditor.json poeditor.tmp "$filename".tmp
}
for file in ${I18N_DIR}/*.json; do
name=$(basename "$file")
code=$(echo "$name" | cut -f1 -d.)
lang=$(jq -r .languageName < "$file")
echo "Downloading $lang ($code)"
check_lang_diff "$code"
done

View File

@@ -8,19 +8,19 @@ 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 }}
POEDITOR_APIKEY: ${{ secrets.POEDITOR_APIKEY }}
run: |
./update-translations.sh
.github/workflows/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

3
.gitignore vendored
View File

@@ -24,4 +24,5 @@ navidrome.db-wal
tags
.gitinfo
docker-compose.yml
!contrib/docker-compose.yml
!contrib/docker-compose.yml
test-123.db

View File

@@ -1,17 +1,14 @@
run:
go: "1.20"
linters:
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- copyloopvar
- dogsled
- durationcheck
- errcheck
- errorlint
- exportloopref
- gocyclo
- goprintffuncname
- gosec
@@ -28,8 +25,13 @@ linters:
- unused
- whitespace
issues:
exclude-rules:
- linters:
- gosec
text: "(G501|G401|G505):"
linters-settings:
govet:
enable:
- nilness
gosec:
excludes:
- G501
- G401
- G505
- G115 # Can't check context, where the warning is clearly a false positive. See discussion in https://github.com/securego/gosec/pull/1149

View File

@@ -1,5 +1,6 @@
# GoReleaser config
project_name: navidrome
version: 2
builds:
- id: navidrome_linux_amd64
@@ -121,7 +122,7 @@ checksum:
name_template: "{{ .ProjectName }}_checksums.txt"
snapshot:
name_template: "{{ .Tag }}-SNAPSHOT"
version_template: "{{ .Tag }}-SNAPSHOT"
release:
draft: true

2
.nvmrc
View File

@@ -1 +1 @@
v18
v20

View File

@@ -9,7 +9,9 @@ GIT_SHA=source_archive
GIT_TAG=$(patsubst navidrome-%,v%,$(notdir $(PWD)))
endif
CI_RELEASER_VERSION=1.21.0-1 ## https://github.com/navidrome/ci-goreleaser
CI_RELEASER_VERSION ?= 1.23.0-1 ## https://github.com/navidrome/ci-goreleaser
UI_SRC_FILES := $(shell find ui -type f -not -path "ui/build/*" -not -path "ui/node_modules/*")
setup: check_env download-deps setup-git ##@1_Run_First Install dependencies and prepare development environment
@echo Downloading Node dependencies...
@@ -20,7 +22,7 @@ dev: check_env ##@Development Start Navidrome in development mode, with hot-re
npx foreman -j Procfile.dev -p 4533 start
.PHONY: dev
server: check_go_env ##@Development Start the backend in development mode
server: check_go_env buildjs ##@Development Start the backend in development mode
@go run github.com/cespare/reflex@latest -d none -c reflex.conf
.PHONY: server
@@ -47,7 +49,7 @@ lintall: lint ##@Development Lint Go and JS code
format: ##@Development Format code
@(cd ./ui && npm run prettier)
@go run golang.org/x/tools/cmd/goimports -w `find . -name '*.go' | grep -v _gen.go$$`
@go run golang.org/x/tools/cmd/goimports@latest -w `find . -name '*.go' | grep -v _gen.go$$`
@go mod tidy
.PHONY: format
@@ -61,12 +63,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
@@ -78,41 +80,46 @@ setup-git: ##@Development Setup Git hooks (pre-commit and pre-push)
@(cd .git/hooks && ln -sf ../../git/* .)
.PHONY: setup-git
buildall: buildjs build ##@Build Build the project, both frontend and backend
.PHONY: buildall
build: warning-noui-build check_go_env ##@Build Build only backend
build: check_go_env buildjs ##@Build Build the project
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)
buildall: deprecated build
.PHONY: buildall
debug-build: check_go_env buildjs ##@Build Build the project (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)
buildjs: check_node_env ui/build/index.html ##@Build Build only frontend
.PHONY: buildjs
all: warning-noui-build ##@Cross_Compilation Build binaries for all supported platforms. It does not build the frontend
ui/build/index.html: $(UI_SRC_FILES)
@(cd ./ui && npm run build)
all: buildjs ##@Cross_Compilation Build binaries for all supported platforms.
@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
single: buildjs ##@Cross_Compilation Build binaries for a single supported platforms.
@if [ -z "${GOOS}" -o -z "${GOARCH}" ]; then \
echo "Usage: GOOS=<os> GOARCH=<arch> make single"; \
echo "Options:"; \
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
warning-noui-build:
@echo "WARNING: This command does not build the frontend, it uses the latest built with 'make buildjs'"
.PHONY: warning-noui-build
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
get-music: ##@Development Download some free music from Navidrome's demo instance
mkdir -p music
@@ -168,6 +175,10 @@ check_node_env:
pre-push: lintall testall
.PHONY: pre-push
deprecated:
@echo "WARNING: This target is deprecated and will be removed in future releases. Use 'make build' instead."
.PHONY: deprecated
.DEFAULT_GOAL := help
HELP_FUN = \

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

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,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)
}
}
@@ -183,6 +201,7 @@ func init() {
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")
@@ -191,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`")
@@ -199,6 +219,7 @@ 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"))

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,22 +24,21 @@ 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:
func CreateServer(musicFolder string) *server.Server {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
dbDB := db.Db()
dataStore := persistence.New(dbDB)
broker := events.GetBroker()
serverServer := server.New(dataStore, broker)
return serverServer
}
func CreateNativeAPIRouter() *nativeapi.Router {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
dbDB := db.Db()
dataStore := persistence.New(dbDB)
share := core.NewShare(dataStore)
playlists := core.NewPlaylists(dataStore)
router := nativeapi.New(dataStore, share, playlists)
@@ -46,8 +46,8 @@ func CreateNativeAPIRouter() *nativeapi.Router {
}
func CreateSubsonicAPIRouter() *subsonic.Router {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
dbDB := db.Db()
dataStore := persistence.New(dbDB)
fileCache := artwork.GetImageCache()
fFmpeg := ffmpeg.New()
agentsAgents := agents.New(dataStore)
@@ -58,17 +58,19 @@ 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
}
func CreatePublicRouter() *public.Router {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
dbDB := db.Db()
dataStore := persistence.New(dbDB)
fileCache := artwork.GetImageCache()
fFmpeg := ffmpeg.New()
agentsAgents := agents.New(dataStore)
@@ -83,22 +85,22 @@ func CreatePublicRouter() *public.Router {
}
func CreateLastFMRouter() *lastfm.Router {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
dbDB := db.Db()
dataStore := persistence.New(dbDB)
router := lastfm.NewRouter(dataStore)
return router
}
func CreateListenBrainzRouter() *listenbrainz.Router {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
dbDB := db.Db()
dataStore := persistence.New(dbDB)
router := listenbrainz.NewRouter(dataStore)
return router
}
func createScanner() scanner.Scanner {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
func GetScanner() scanner.Scanner {
dbDB := db.Db()
dataStore := persistence.New(dbDB)
playlists := core.NewPlaylists(dataStore)
fileCache := artwork.GetImageCache()
fFmpeg := ffmpeg.New()
@@ -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 {
dbDB := db.Db()
dataStore := persistence.New(dbDB)
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,74 +12,81 @@ 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"
)
type configOptions struct {
ConfigFile string
Address string
Port int
MusicFolder string
DataFolder string
CacheFolder string
DbPath string
LogLevel string
ScanInterval time.Duration
ScanSchedule string
SessionTimeout time.Duration
BaseURL string
BasePath string
BaseHost string
BaseScheme string
TLSCert string
TLSKey string
UILoginBackgroundURL string
UIWelcomeMessage string
MaxSidebarPlaylists int
EnableTranscodingConfig bool
EnableDownloads bool
EnableExternalServices bool
EnableMediaFileCoverArt bool
TranscodingCacheSize string
ImageCacheSize string
EnableArtworkPrecache bool
AutoImportPlaylists bool
PlaylistsPath string
AutoTranscodeDownload bool
DefaultDownsamplingFormat string
SearchFullString bool
RecentlyAddedByModTime bool
IgnoredArticles string
IndexGroups string
SubsonicArtistParticipations bool
FFmpegPath string
MPVPath 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
EnableReplayGain bool
EnableCoverAnimation bool
GATrackingID string
EnableLogRedacting bool
AuthRequestLimit int
AuthWindowLength time.Duration
PasswordEncryptionKey string
ReverseProxyUserHeader string
ReverseProxyWhitelist string
Prometheus prometheusOptions
Scanner scannerOptions
Jukebox jukeboxOptions
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
EnableTranscodingConfig bool
EnableDownloads bool
EnableExternalServices bool
EnableMediaFileCoverArt bool
TranscodingCacheSize string
ImageCacheSize string
AlbumPlayCountMode string
EnableArtworkPrecache bool
AutoImportPlaylists bool
DefaultPlaylistPublicVisibility bool
PlaylistsPath string
SmartPlaylistRefreshDelay time.Duration
AutoTranscodeDownload bool
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
ShareURL string
DefaultDownloadableShare bool
DefaultTheme string
DefaultLanguage string
DefaultUIVolume int
EnableReplayGain bool
EnableCoverAnimation bool
GATrackingID string
EnableLogRedacting bool
AuthRequestLimit int
AuthWindowLength time.Duration
PasswordEncryptionKey string
ReverseProxyUserHeader string
ReverseProxyWhitelist string
HTTPSecurityHeaders secureOptions
Prometheus prometheusOptions
Scanner scannerOptions
Jukebox jukeboxOptions
Agents string
LastFM lastfmOptions
@@ -96,6 +103,7 @@ type configOptions struct {
DevSidebarPlaylists bool
DevEnableBufferedScrobble bool
DevShowArtistPage bool
DevOffsetOptimize int
DevArtworkMaxRequests int
DevArtworkThrottleBacklogLimit int
DevArtworkThrottleBacklogTimeout time.Duration
@@ -126,6 +134,10 @@ type listenBrainzOptions struct {
BaseURL string
}
type secureOptions struct {
CustomFrameOptionsValue string
}
type prometheusOptions struct {
Enabled bool
MetricsPath string
@@ -134,9 +146,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 (
@@ -203,7 +216,7 @@ func Load() {
}
// 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)
@@ -273,6 +286,7 @@ func init() {
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")
@@ -285,9 +299,12 @@ 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("defaultplaylistpublicvisibility", false)
viper.SetDefault("playlistspath", consts.DefaultPlaylistsPath)
viper.SetDefault("smartPlaylistRefreshDelay", 5*time.Second)
viper.SetDefault("enabledownloads", true)
viper.SetDefault("enableexternalservices", true)
viper.SetDefault("enablemediafilecoverart", true)
@@ -295,10 +312,13 @@ func init() {
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")
@@ -326,6 +346,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", ";/,")
@@ -334,13 +355,15 @@ func init() {
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)
@@ -348,11 +371,13 @@ func init() {
viper.SetDefault("devautologinusername", "")
viper.SetDefault("devactivitypanel", true)
viper.SetDefault("enablesharing", false)
viper.SetDefault("shareurl", "")
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

@@ -11,7 +11,7 @@ import (
const (
AppName = "navidrome"
DefaultDbPath = "navidrome.db?cache=shared&_busy_timeout=15000&_journal_mode=WAL&_foreign_keys=on"
DefaultDbPath = "navidrome.db?cache=shared&_cache_size=1000000000&_busy_timeout=5000&_journal_mode=WAL&_synchronous=NORMAL&_foreign_keys=on&_txlock=immediate"
InitialSetupFlagKey = "InitialSetup"
UIAuthorizationHeader = "X-ND-Authorization"
@@ -81,32 +81,36 @@ const (
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: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 -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 -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,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

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

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

@@ -60,7 +60,7 @@ var _ = Describe("Agents", func() {
Describe("GetArtistMBID", func() {
It("returns on first match", func() {
Expect(ag.GetArtistMBID(ctx, "123", "test")).To(Equal("mbid"))
Expect(mock.Args).To(ConsistOf("123", "test"))
Expect(mock.Args).To(HaveExactElements("123", "test"))
})
It("returns empty if artist is Various Artists", func() {
mbid, err := ag.GetArtistMBID(ctx, consts.VariousArtistsID, consts.VariousArtists)
@@ -78,7 +78,7 @@ var _ = Describe("Agents", func() {
mock.Err = errors.New("error")
_, err := ag.GetArtistMBID(ctx, "123", "test")
Expect(err).To(MatchError(ErrNotFound))
Expect(mock.Args).To(ConsistOf("123", "test"))
Expect(mock.Args).To(HaveExactElements("123", "test"))
})
It("interrupts if the context is canceled", func() {
cancel()
@@ -91,7 +91,7 @@ var _ = Describe("Agents", func() {
Describe("GetArtistURL", func() {
It("returns on first match", func() {
Expect(ag.GetArtistURL(ctx, "123", "test", "mb123")).To(Equal("url"))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
Expect(mock.Args).To(HaveExactElements("123", "test", "mb123"))
})
It("returns empty if artist is Various Artists", func() {
url, err := ag.GetArtistURL(ctx, consts.VariousArtistsID, consts.VariousArtists, "")
@@ -109,7 +109,7 @@ var _ = Describe("Agents", func() {
mock.Err = errors.New("error")
_, err := ag.GetArtistURL(ctx, "123", "test", "mb123")
Expect(err).To(MatchError(ErrNotFound))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
Expect(mock.Args).To(HaveExactElements("123", "test", "mb123"))
})
It("interrupts if the context is canceled", func() {
cancel()
@@ -122,7 +122,7 @@ var _ = Describe("Agents", func() {
Describe("GetArtistBiography", func() {
It("returns on first match", func() {
Expect(ag.GetArtistBiography(ctx, "123", "test", "mb123")).To(Equal("bio"))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
Expect(mock.Args).To(HaveExactElements("123", "test", "mb123"))
})
It("returns empty if artist is Various Artists", func() {
bio, err := ag.GetArtistBiography(ctx, consts.VariousArtistsID, consts.VariousArtists, "")
@@ -140,7 +140,7 @@ var _ = Describe("Agents", func() {
mock.Err = errors.New("error")
_, err := ag.GetArtistBiography(ctx, "123", "test", "mb123")
Expect(err).To(MatchError(ErrNotFound))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
Expect(mock.Args).To(HaveExactElements("123", "test", "mb123"))
})
It("interrupts if the context is canceled", func() {
cancel()
@@ -156,13 +156,13 @@ var _ = Describe("Agents", func() {
URL: "imageUrl",
Size: 100,
}}))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
Expect(mock.Args).To(HaveExactElements("123", "test", "mb123"))
})
It("skips the agent if it returns an error", func() {
mock.Err = errors.New("error")
_, err := ag.GetArtistImages(ctx, "123", "test", "mb123")
Expect(err).To(MatchError("not found"))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
Expect(mock.Args).To(HaveExactElements("123", "test", "mb123"))
})
It("interrupts if the context is canceled", func() {
cancel()
@@ -178,13 +178,13 @@ var _ = Describe("Agents", func() {
Name: "Joe Dohn",
MBID: "mbid321",
}}))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123", 1))
Expect(mock.Args).To(HaveExactElements("123", "test", "mb123", 1))
})
It("skips the agent if it returns an error", func() {
mock.Err = errors.New("error")
_, err := ag.GetSimilarArtists(ctx, "123", "test", "mb123", 1)
Expect(err).To(MatchError(ErrNotFound))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123", 1))
Expect(mock.Args).To(HaveExactElements("123", "test", "mb123", 1))
})
It("interrupts if the context is canceled", func() {
cancel()
@@ -200,13 +200,13 @@ var _ = Describe("Agents", func() {
Name: "A Song",
MBID: "mbid444",
}}))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123", 2))
Expect(mock.Args).To(HaveExactElements("123", "test", "mb123", 2))
})
It("skips the agent if it returns an error", func() {
mock.Err = errors.New("error")
_, err := ag.GetArtistTopSongs(ctx, "123", "test", "mb123", 2)
Expect(err).To(MatchError(ErrNotFound))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123", 2))
Expect(mock.Args).To(HaveExactElements("123", "test", "mb123", 2))
})
It("interrupts if the context is canceled", func() {
cancel()
@@ -236,13 +236,13 @@ var _ = Describe("Agents", func() {
},
},
}))
Expect(mock.Args).To(ConsistOf("album", "artist", "mbid"))
Expect(mock.Args).To(HaveExactElements("album", "artist", "mbid"))
})
It("skips the agent if it returns an error", func() {
mock.Err = errors.New("error")
_, err := ag.GetAlbumInfo(ctx, "album", "artist", "mbid")
Expect(err).To(MatchError(ErrNotFound))
Expect(mock.Args).To(ConsistOf("album", "artist", "mbid"))
Expect(mock.Args).To(HaveExactElements("album", "artist", "mbid"))
})
It("interrupts if the context is canceled", func() {
cancel()

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

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

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

@@ -150,7 +150,7 @@ func (a *archiver) addFileToZip(ctx context.Context, z *zip.Writer, mf model.Med
var r io.ReadCloser
if format != "raw" && format != "" {
r, err = a.ms.DoStream(ctx, &mf, format, bitrate)
r, err = a.ms.DoStream(ctx, &mf, format, bitrate, 0)
} else {
r, err = os.Open(mf.Path)
}
@@ -160,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)
}
}()

View File

@@ -44,7 +44,7 @@ var _ = Describe("Archiver", func() {
}}).Return(mfs, nil)
ds.On("MediaFile", mock.Anything).Return(mfRepo)
ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128).Return(io.NopCloser(strings.NewReader("test")), nil).Times(3)
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)
@@ -73,7 +73,7 @@ var _ = Describe("Archiver", func() {
}}).Return(mfs, nil)
ds.On("MediaFile", mock.Anything).Return(mfRepo)
ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
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)
@@ -104,7 +104,7 @@ var _ = Describe("Archiver", func() {
}
sh.On("Load", mock.Anything, "1").Return(share, nil)
ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
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)
@@ -136,7 +136,7 @@ var _ = Describe("Archiver", func() {
plRepo := &mockPlaylistRepository{}
plRepo.On("GetWithTracks", "1", true).Return(pls, nil)
ds.On("Playlist", mock.Anything).Return(plRepo)
ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
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)
@@ -192,8 +192,8 @@ type mockMediaStreamer struct {
core.MediaStreamer
}
func (m *mockMediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, format string, bitrate int) (*core.Stream, error) {
args := m.Called(ctx, mf, format, bitrate)
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)
}

View File

@@ -20,8 +20,8 @@ import (
var ErrUnavailable = errors.New("artwork unavailable")
type Artwork interface {
Get(ctx context.Context, artID model.ArtworkID, size int) (io.ReadCloser, time.Time, error)
GetOrPlaceholder(ctx context.Context, id string, size int) (io.ReadCloser, time.Time, error)
Get(ctx context.Context, artID model.ArtworkID, size int, square bool) (io.ReadCloser, time.Time, error)
GetOrPlaceholder(ctx context.Context, id string, size int, square bool) (io.ReadCloser, time.Time, error)
}
func NewArtwork(ds model.DataStore, cache cache.FileCache, ffmpeg ffmpeg.FFmpeg, em core.ExternalMetadata) Artwork {
@@ -41,10 +41,10 @@ type artworkReader interface {
Reader(ctx context.Context) (io.ReadCloser, string, error)
}
func (a *artwork) GetOrPlaceholder(ctx context.Context, id string, size int) (reader io.ReadCloser, lastUpdate time.Time, err error) {
func (a *artwork) GetOrPlaceholder(ctx context.Context, id string, size int, square bool) (reader io.ReadCloser, lastUpdate time.Time, err error) {
artID, err := a.getArtworkId(ctx, id)
if err == nil {
reader, lastUpdate, err = a.Get(ctx, artID, size)
reader, lastUpdate, err = a.Get(ctx, artID, size, square)
}
if errors.Is(err, ErrUnavailable) {
if artID.Kind == model.KindArtistArtwork {
@@ -57,8 +57,8 @@ func (a *artwork) GetOrPlaceholder(ctx context.Context, id string, size int) (re
return reader, lastUpdate, err
}
func (a *artwork) Get(ctx context.Context, artID model.ArtworkID, size int) (reader io.ReadCloser, lastUpdate time.Time, err error) {
artReader, err := a.getArtworkReader(ctx, artID, size)
func (a *artwork) Get(ctx context.Context, artID model.ArtworkID, size int, square bool) (reader io.ReadCloser, lastUpdate time.Time, err error) {
artReader, err := a.getArtworkReader(ctx, artID, size, square)
if err != nil {
return nil, time.Time{}, err
}
@@ -107,11 +107,11 @@ func (a *artwork) getArtworkId(ctx context.Context, id string) (model.ArtworkID,
return artID, nil
}
func (a *artwork) getArtworkReader(ctx context.Context, artID model.ArtworkID, size int) (artworkReader, error) {
func (a *artwork) getArtworkReader(ctx context.Context, artID model.ArtworkID, size int, square bool) (artworkReader, error) {
var artReader artworkReader
var err error
if size > 0 {
artReader, err = resizedFromOriginal(ctx, a, artID, size)
if size > 0 || square {
artReader, err = resizedFromOriginal(ctx, a, artID, size, square)
} else {
switch artID.Kind {
case model.KindArtistArtwork:

View File

@@ -4,7 +4,11 @@ import (
"context"
"errors"
"image"
"image/jpeg"
"image/png"
"io"
"os"
"path/filepath"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
@@ -211,33 +215,83 @@ var _ = Describe("Artwork", func() {
alMultipleCovers,
})
})
It("returns a PNG if original image is a PNG", func() {
conf.Server.CoverArtPriority = "front.png"
r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 15)
Expect(err).ToNot(HaveOccurred())
When("Square is false", func() {
It("returns a PNG if original image is a PNG", func() {
conf.Server.CoverArtPriority = "front.png"
r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 15, false)
Expect(err).ToNot(HaveOccurred())
br, format, err := asImageReader(r)
Expect(format).To(Equal("image/png"))
Expect(err).ToNot(HaveOccurred())
img, format, err := image.Decode(r)
Expect(err).ToNot(HaveOccurred())
Expect(format).To(Equal("png"))
Expect(img.Bounds().Size().X).To(Equal(15))
Expect(img.Bounds().Size().Y).To(Equal(15))
})
It("returns a JPEG if original image is not a PNG", func() {
conf.Server.CoverArtPriority = "cover.jpg"
r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 200, false)
Expect(err).ToNot(HaveOccurred())
img, _, err := image.Decode(br)
Expect(err).ToNot(HaveOccurred())
Expect(img.Bounds().Size().X).To(Equal(15))
Expect(img.Bounds().Size().Y).To(Equal(15))
img, format, err := image.Decode(r)
Expect(format).To(Equal("jpeg"))
Expect(err).ToNot(HaveOccurred())
Expect(img.Bounds().Size().X).To(Equal(200))
Expect(img.Bounds().Size().Y).To(Equal(200))
})
})
It("returns a JPEG if original image is not a PNG", func() {
conf.Server.CoverArtPriority = "cover.jpg"
r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 200)
Expect(err).ToNot(HaveOccurred())
When("When square is true", func() {
var alCover model.Album
br, format, err := asImageReader(r)
Expect(format).To(Equal("image/jpeg"))
Expect(err).ToNot(HaveOccurred())
DescribeTable("resize",
func(format string, landscape bool, size int) {
coverFileName := "cover." + format
dirName := createImage(format, landscape, size)
alCover = model.Album{
ID: "444",
Name: "Only external",
ImageFiles: filepath.Join(dirName, coverFileName),
}
ds.Album(ctx).(*tests.MockAlbumRepo).SetData(model.Albums{
alCover,
})
img, _, err := image.Decode(br)
Expect(err).ToNot(HaveOccurred())
Expect(img.Bounds().Size().X).To(Equal(200))
Expect(img.Bounds().Size().Y).To(Equal(200))
conf.Server.CoverArtPriority = coverFileName
r, _, err := aw.Get(context.Background(), alCover.CoverArtID(), size, true)
Expect(err).ToNot(HaveOccurred())
img, format, err := image.Decode(r)
Expect(err).ToNot(HaveOccurred())
Expect(format).To(Equal("png"))
Expect(img.Bounds().Size().X).To(Equal(size))
Expect(img.Bounds().Size().Y).To(Equal(size))
},
Entry("portrait png image", "png", false, 200),
Entry("landscape png image", "png", true, 200),
Entry("portrait jpg image", "jpg", false, 200),
Entry("landscape jpg image", "jpg", true, 200),
)
})
})
})
func createImage(format string, landscape bool, size int) string {
var img image.Image
if landscape {
img = image.NewRGBA(image.Rect(0, 0, size, size/2))
} else {
img = image.NewRGBA(image.Rect(0, 0, size/2, size))
}
tmpDir := GinkgoT().TempDir()
f, _ := os.Create(filepath.Join(tmpDir, "cover."+format))
defer f.Close()
switch format {
case "png":
_ = png.Encode(f, img)
case "jpg":
_ = jpeg.Encode(f, img, &jpeg.Options{Quality: 75})
}
return tmpDir
}

View File

@@ -31,7 +31,7 @@ var _ = Describe("Artwork", func() {
Context("GetOrPlaceholder", func() {
Context("Empty ID", func() {
It("returns placeholder if album is not in the DB", func() {
r, _, err := aw.GetOrPlaceholder(context.Background(), "", 0)
r, _, err := aw.GetOrPlaceholder(context.Background(), "", 0, false)
Expect(err).ToNot(HaveOccurred())
ph, err := resources.FS().Open(consts.PlaceholderAlbumArt)
@@ -49,7 +49,7 @@ var _ = Describe("Artwork", func() {
Context("Get", func() {
Context("Empty ID", func() {
It("returns an ErrUnavailable error", func() {
_, _, err := aw.Get(context.Background(), model.ArtworkID{}, 0)
_, _, err := aw.Get(context.Background(), model.ArtworkID{}, 0, false)
Expect(err).To(MatchError(artwork.ErrUnavailable))
})
})

View File

@@ -4,6 +4,8 @@ import (
"context"
"fmt"
"io"
"maps"
"slices"
"sync"
"time"
@@ -14,7 +16,6 @@ import (
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/utils/cache"
"github.com/navidrome/navidrome/utils/pl"
"golang.org/x/exp/maps"
)
type CacheWarmer interface {
@@ -94,7 +95,7 @@ func (a *cacheWarmer) run(ctx context.Context) {
continue
}
batch := maps.Keys(a.buffer)
batch := slices.Collect(maps.Keys(a.buffer))
a.buffer = make(map[model.ArtworkID]struct{})
a.mutex.Unlock()
@@ -121,7 +122,7 @@ func (a *cacheWarmer) processBatch(ctx context.Context, batch []model.ArtworkID)
input := pl.FromSlice(ctx, batch)
errs := pl.Sink(ctx, 2, input, a.doCacheImage)
for err := range errs {
log.Warn(ctx, "Error warming cache", err)
log.Debug(ctx, "Error warming cache", err)
}
}
@@ -129,9 +130,9 @@ func (a *cacheWarmer) doCacheImage(ctx context.Context, id model.ArtworkID) erro
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
r, _, err := a.artwork.Get(ctx, id, consts.UICoverArtSize)
r, _, err := a.artwork.Get(ctx, id, consts.UICoverArtSize, false)
if err != nil {
return fmt.Errorf("error cacheing id='%s': %w", id, err)
return fmt.Errorf("caching id='%s': %w", id, err)
}
defer r.Close()
_, err = io.Copy(io.Discard, r)

View File

@@ -17,7 +17,7 @@ import (
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/str"
)
type artistReader struct {
@@ -56,7 +56,7 @@ func newArtistReader(ctx context.Context, artwork *artwork, artID model.ArtworkI
}
}
a.files = strings.Join(files, consts.Zwsp)
a.artistFolder = utils.LongestCommonPrefix(paths)
a.artistFolder = str.LongestCommonPrefix(paths)
if !strings.HasSuffix(a.artistFolder, string(filepath.Separator)) {
a.artistFolder, _ = filepath.Split(a.artistFolder)
}

View File

@@ -1,7 +1,6 @@
package artwork
import (
"bufio"
"bytes"
"context"
"fmt"
@@ -9,14 +8,12 @@ import (
"image/jpeg"
"image/png"
"io"
"net/http"
"time"
"github.com/disintegration/imaging"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/number"
)
type resizedArtworkReader struct {
@@ -24,16 +21,18 @@ type resizedArtworkReader struct {
cacheKey string
lastUpdate time.Time
size int
square bool
a *artwork
}
func resizedFromOriginal(ctx context.Context, a *artwork, artID model.ArtworkID, size int) (*resizedArtworkReader, error) {
func resizedFromOriginal(ctx context.Context, a *artwork, artID model.ArtworkID, size int, square bool) (*resizedArtworkReader, error) {
r := &resizedArtworkReader{a: a}
r.artID = artID
r.size = size
r.square = square
// Get lastUpdated and cacheKey from original artwork
original, err := a.getArtworkReader(ctx, artID, 0)
original, err := a.getArtworkReader(ctx, artID, 0, false)
if err != nil {
return nil, err
}
@@ -43,12 +42,11 @@ func resizedFromOriginal(ctx context.Context, a *artwork, artID model.ArtworkID,
}
func (a *resizedArtworkReader) Key() string {
return fmt.Sprintf(
"%s.%d.%d",
a.cacheKey,
a.size,
conf.Server.CoverJpegQuality,
)
baseKey := fmt.Sprintf("%s.%d", a.cacheKey, a.size)
if a.square {
return baseKey + ".square"
}
return fmt.Sprintf("%s.%d", baseKey, conf.Server.CoverJpegQuality)
}
func (a *resizedArtworkReader) LastUpdated() time.Time {
@@ -57,7 +55,7 @@ func (a *resizedArtworkReader) LastUpdated() time.Time {
func (a *resizedArtworkReader) Reader(ctx context.Context) (io.ReadCloser, string, error) {
// Get artwork in original size, possibly from cache
orig, _, err := a.a.Get(ctx, a.artID, 0)
orig, _, err := a.a.Get(ctx, a.artID, 0, false)
if err != nil {
return nil, "", err
}
@@ -67,7 +65,7 @@ func (a *resizedArtworkReader) Reader(ctx context.Context) (io.ReadCloser, strin
r := io.TeeReader(orig, buf)
defer orig.Close()
resized, origSize, err := resizeImage(r, a.size)
resized, origSize, err := resizeImage(r, a.size, a.square)
if resized == nil {
log.Trace(ctx, "Image smaller than requested size", "artID", a.artID, "original", origSize, "resized", a.size)
} else {
@@ -84,54 +82,39 @@ func (a *resizedArtworkReader) Reader(ctx context.Context) (io.ReadCloser, strin
return io.NopCloser(resized), fmt.Sprintf("%s@%d", a.artID, a.size), nil
}
func asImageReader(r io.Reader) (io.Reader, string, error) {
br := bufio.NewReader(r)
buf, err := br.Peek(512)
if err == io.EOF && len(buf) > 0 {
// Check if there are enough bytes to detect type
typ := http.DetectContentType(buf)
if typ != "" {
return br, typ, nil
}
}
if err != nil {
return nil, "", err
}
return br, http.DetectContentType(buf), nil
}
func resizeImage(reader io.Reader, size int) (io.Reader, int, error) {
r, format, err := asImageReader(reader)
func resizeImage(reader io.Reader, size int, square bool) (io.Reader, int, error) {
original, format, err := image.Decode(reader)
if err != nil {
return nil, 0, err
}
img, _, err := image.Decode(r)
if err != nil {
return nil, 0, err
}
bounds := original.Bounds()
originalSize := max(bounds.Max.X, bounds.Max.Y)
// Don't upscale the image
bounds := img.Bounds()
originalSize := number.Max(bounds.Max.X, bounds.Max.Y)
if originalSize <= size {
if originalSize <= size && !square {
return nil, originalSize, nil
}
var m *image.NRGBA
// Preserve the aspect ratio of the image.
if bounds.Max.X > bounds.Max.Y {
m = imaging.Resize(img, size, 0, imaging.Lanczos)
var resized image.Image
if originalSize >= size {
resized = imaging.Fit(original, size, size, imaging.Lanczos)
} else {
m = imaging.Resize(img, 0, size, imaging.Lanczos)
if bounds.Max.Y < bounds.Max.X {
resized = imaging.Resize(original, size, 0, imaging.Lanczos)
} else {
resized = imaging.Resize(original, 0, size, imaging.Lanczos)
}
}
if square {
bg := image.NewRGBA(image.Rect(0, 0, size, size))
resized = imaging.OverlayCenter(bg, resized, 1)
}
buf := new(bytes.Buffer)
buf.Reset()
if format == "image/png" {
err = png.Encode(buf, m)
if format == "png" || square {
err = png.Encode(buf, resized)
} else {
err = jpeg.Encode(buf, m, &jpeg.Options{Quality: conf.Server.CoverJpegQuality})
err = jpeg.Encode(buf, resized, &jpeg.Options{Quality: conf.Server.CoverJpegQuality})
}
return buf, originalSize, err
}

View File

@@ -37,7 +37,7 @@ func selectImageReader(ctx context.Context, artID model.ArtworkID, extractFuncs
}
log.Trace(ctx, "Failed trying to extract artwork", "artID", artID, "source", f, "elapsed", time.Since(start), err)
}
return nil, "", fmt.Errorf("could not get a cover art for %s: %w", artID, ErrUnavailable)
return nil, "", fmt.Errorf("could not get `%s` cover art for %s: %w", artID.Kind, artID, ErrUnavailable)
}
type sourceFunc func() (r io.ReadCloser, path string, err error)
@@ -124,7 +124,7 @@ func fromFFmpegTag(ctx context.Context, ffmpeg ffmpeg.FFmpeg, path string) sourc
func fromAlbum(ctx context.Context, a *artwork, id model.ArtworkID) sourceFunc {
return func() (io.ReadCloser, string, error) {
r, _, err := a.Get(ctx, id, 0)
r, _, err := a.Get(ctx, id, 0, false)
if err != nil {
return nil, "", err
}

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,9 @@ 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"
"github.com/navidrome/navidrome/utils/str"
"golang.org/x/sync/errgroup"
)
@@ -42,8 +43,8 @@ type ExternalMetadata interface {
type externalMetadata struct {
ds model.DataStore
ag *agents.Agents
artistQueue chan<- *auxArtist
albumQueue chan<- *auxAlbum
artistQueue refreshQueue[auxArtist]
albumQueue refreshQueue[auxAlbum]
}
type auxAlbum struct {
@@ -58,8 +59,8 @@ type auxArtist struct {
func NewExternalMetadata(ds model.DataStore, agents *agents.Agents) ExternalMetadata {
e := &externalMetadata{ds: ds, ag: agents}
e.artistQueue = startRefreshQueue(context.TODO(), e.populateArtistInfo)
e.albumQueue = startRefreshQueue(context.TODO(), e.populateAlbumInfo)
e.artistQueue = newRefreshQueue(context.TODO(), e.populateArtistInfo)
e.albumQueue = newRefreshQueue(context.TODO(), e.populateAlbumInfo)
return e
}
@@ -74,7 +75,7 @@ func (e *externalMetadata) getAlbum(ctx context.Context, id string) (*auxAlbum,
switch v := entity.(type) {
case *model.Album:
album.Album = *v
album.Name = clearName(v.Name)
album.Name = str.Clear(v.Name)
case *model.MediaFile:
return e.getAlbum(ctx, v.AlbumID)
default:
@@ -90,17 +91,19 @@ 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 info is expired, trigger a populateAlbumInfo in the background
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)
e.albumQueue.enqueue(*album)
}
return &album.Album, nil
@@ -118,7 +121,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 != "" {
@@ -163,7 +166,7 @@ func (e *externalMetadata) getArtist(ctx context.Context, id string) (*auxArtist
switch v := entity.(type) {
case *model.Artist:
artist.Artist = *v
artist.Name = clearName(v.Name)
artist.Name = str.Clear(v.Name)
case *model.MediaFile:
return e.getArtist(ctx, v.ArtistID)
case *model.Album:
@@ -174,17 +177,6 @@ func (e *externalMetadata) getArtist(ctx context.Context, id string) (*auxArtist
return &artist, nil
}
// Replace some Unicode chars with their equivalent ASCII
func clearName(name string) string {
name = strings.ReplaceAll(name, "", "-")
name = strings.ReplaceAll(name, "", "-")
name = strings.ReplaceAll(name, "“", `"`)
name = strings.ReplaceAll(name, "”", `"`)
name = strings.ReplaceAll(name, "", `'`)
name = strings.ReplaceAll(name, "", `'`)
return name
}
func (e *externalMetadata) UpdateArtistInfo(ctx context.Context, id string, similarCount int, includeNotPresent bool) (*model.Artist, error) {
artist, err := e.refreshArtistInfo(ctx, id)
if err != nil {
@@ -202,8 +194,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,9 +204,9 @@ 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)
enqueueRefresh(e.artistQueue, artist)
if time.Since(updatedAt) > conf.Server.DevArtistInfoTimeToLive {
log.Debug("Found expired cached ArtistInfo, refreshing in the background", "updatedAt", updatedAt, "name", artist.Name)
e.artistQueue.enqueue(*artist)
}
return artist, nil
}
@@ -242,7 +235,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 +258,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.NewWeightedChooser[model.MediaFile]()
addArtist := func(a model.Artist, weightedSongs *random.WeightedChooser[model.MediaFile], 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)
@@ -300,12 +293,12 @@ func (e *externalMetadata) SimilarSongs(ctx context.Context, id string, count in
var similarSongs model.MediaFiles
for len(similarSongs) < count && weightedSongs.Size() > 0 {
s, err := weightedSongs.GetAndRemove()
s, err := weightedSongs.Pick()
if err != nil {
log.Warn(ctx, "Error getting weighted song", err)
continue
}
similarSongs = append(similarSongs, s.(model.MediaFile))
similarSongs = append(similarSongs, s)
}
return similarSongs, nil
@@ -412,7 +405,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": str.SanitizeFieldForSorting(title)},
},
Sort: "starred desc, rating desc, year asc, compilation asc ",
Max: 1,
@@ -432,11 +425,11 @@ func (e *externalMetadata) callGetURL(ctx context.Context, agent agents.ArtistUR
}
func (e *externalMetadata) callGetBiography(ctx context.Context, agent agents.ArtistBiographyRetriever, artist *auxArtist) {
bio, err := agent.GetArtistBiography(ctx, artist.ID, clearName(artist.Name), artist.MbzArtistID)
bio, err := agent.GetArtistBiography(ctx, artist.ID, str.Clear(artist.Name), artist.MbzArtistID)
if err != nil {
return
}
bio = utils.SanitizeText(bio)
bio = str.SanitizeText(bio)
bio = strings.ReplaceAll(bio, "\n", " ")
artist.Biography = strings.ReplaceAll(bio, "<a ", "<a target='_blank' ")
}
@@ -512,7 +505,7 @@ func (e *externalMetadata) findArtistByName(ctx context.Context, artistName stri
}
artist := &auxArtist{
Artist: artists[0],
Name: clearName(artists[0].Name),
Name: str.Clear(artists[0].Name),
}
return artist, nil
}
@@ -559,15 +552,17 @@ func (e *externalMetadata) loadSimilar(ctx context.Context, artist *auxArtist, c
return nil
}
func startRefreshQueue[T any](ctx context.Context, processFn func(context.Context, T) error) chan<- T {
type refreshQueue[T any] chan<- T
func newRefreshQueue[T any](ctx context.Context, processFn func(context.Context, *T) error) refreshQueue[T] {
queue := make(chan T, refreshQueueLength)
go func() {
for {
time.Sleep(refreshDelay)
ctx, cancel := context.WithTimeout(ctx, refreshTimeout)
select {
case a := <-queue:
_ = processFn(ctx, a)
case item := <-queue:
_ = processFn(ctx, &item)
cancel()
case <-ctx.Done():
cancel()
@@ -578,9 +573,9 @@ func startRefreshQueue[T any](ctx context.Context, processFn func(context.Contex
return queue
}
func enqueueRefresh[T any](queue chan<- T, item T) {
func (q *refreshQueue[T]) enqueue(item T) {
select {
case queue <- item:
default: // It is ok to miss a refresh
case *q <- item:
default: // It is ok to miss a refresh request
}
}

View File

@@ -16,12 +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 {
@@ -37,11 +39,11 @@ const (
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)
}
@@ -49,17 +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)
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)
args := createFFmpegCommand(createFLACCmd, path, 0, 0)
return e.start(ctx, args)
}
@@ -78,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}
@@ -100,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
@@ -127,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

@@ -1,6 +1,7 @@
package core
import (
"cmp"
"context"
"fmt"
"io"
@@ -19,8 +20,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 +41,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 +72,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 +90,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 +103,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())
@@ -125,56 +128,64 @@ func (s *Stream) EstimatedContentLength() int {
return int(s.mf.Duration * float32(s.bitRate) / 8 * 1024)
}
// TODO This function deserves some love (refactoring)
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 format, 0
// selectTranscodingOptions selects the appropriate transcoding options based on the requested format and bitrate.
// If the requested format is "raw" or matches the media file's suffix and the requested bitrate is 0, it returns the
// original format and bitrate.
// Otherwise, it determines the format and bitrate using determineFormatAndBitRate and findTranscoding functions.
//
// NOTE: It is easier to follow the tests in core/media_streamer_internal_test.go to understand the different scenarios.
func selectTranscodingOptions(ctx context.Context, ds model.DataStore, mf *model.MediaFile, reqFormat string, reqBitRate int) (string, int) {
if reqFormat == "raw" || reqFormat == mf.Suffix && reqBitRate == 0 {
return "raw", mf.BitRate
}
if reqFormat == mf.Suffix && reqBitRate == 0 {
bitRate = mf.BitRate
return format, bitRate
format, bitRate := determineFormatAndBitRate(ctx, mf.BitRate, reqFormat, reqBitRate)
if format == "" && bitRate == 0 {
return "raw", 0
}
trc, hasDefault := request.TranscodingFrom(ctx)
var cFormat string
var cBitRate int
return findTranscoding(ctx, ds, mf, format, bitRate)
}
// determineFormatAndBitRate determines the format and bitrate for transcoding based on the requested format and bitrate.
// If the requested format is not empty, it returns the requested format and bitrate.
// Otherwise, it checks for default transcoding settings from the context or server configuration.
func determineFormatAndBitRate(ctx context.Context, srcBitRate int, reqFormat string, reqBitRate int) (string, int) {
if reqFormat != "" {
cFormat = reqFormat
} else {
if hasDefault {
cFormat = trc.TargetFormat
cBitRate = trc.DefaultBitRate
if p, ok := request.PlayerFrom(ctx); ok {
cBitRate = p.MaxBitRate
}
} else if reqBitRate > 0 && reqBitRate < mf.BitRate && conf.Server.DefaultDownsamplingFormat != "" {
// If no format is specified and no transcoding associated to the player, but a bitrate is specified,
// and there is no transcoding set for the player, we use the default downsampling format.
// But only if the requested bitRate is lower than the original bitRate.
log.Debug("Default Downsampling", "Using default downsampling format", conf.Server.DefaultDownsamplingFormat)
cFormat = conf.Server.DefaultDownsamplingFormat
return reqFormat, reqBitRate
}
format, bitRate := "", 0
if trc, hasDefault := request.TranscodingFrom(ctx); hasDefault {
format = trc.TargetFormat
bitRate = trc.DefaultBitRate
if p, ok := request.PlayerFrom(ctx); ok && p.MaxBitRate > 0 && p.MaxBitRate < bitRate {
bitRate = p.MaxBitRate
}
} else if reqBitRate > 0 && reqBitRate < srcBitRate && conf.Server.DefaultDownsamplingFormat != "" {
// If no format is specified and no transcoding associated to the player, but a bitrate is specified,
// and there is no transcoding set for the player, we use the default downsampling format.
// But only if the requested bitRate is lower than the original bitRate.
log.Debug(ctx, "Using default downsampling format", "format", conf.Server.DefaultDownsamplingFormat)
format = conf.Server.DefaultDownsamplingFormat
}
if reqBitRate > 0 {
cBitRate = reqBitRate
return format, cmp.Or(reqBitRate, bitRate)
}
// findTranscoding finds the appropriate transcoding settings for the given format and bitrate.
// If the format matches the media file's suffix and the bitrate is greater than or equal to the original bitrate,
// it returns the original format and bitrate.
// Otherwise, it returns the target format and bitrate from the
// transcoding settings.
func findTranscoding(ctx context.Context, ds model.DataStore, mf *model.MediaFile, format string, bitRate int) (string, int) {
t, err := ds.Transcoding(ctx).FindByFormat(format)
if err != nil || t == nil || format == mf.Suffix && bitRate >= mf.BitRate {
return "raw", 0
}
if cBitRate == 0 && cFormat == "" {
return format, bitRate
}
t, err := ds.Transcoding(ctx).FindByFormat(cFormat)
if err == nil {
format = t.TargetFormat
if cBitRate != 0 {
bitRate = cBitRate
} else {
bitRate = t.DefaultBitRate
}
}
if format == mf.Suffix && bitRate >= mf.BitRate {
format = "raw"
bitRate = 0
}
return format, bitRate
return t.TargetFormat, cmp.Or(bitRate, t.DefaultBitRate)
}
var (
@@ -199,7 +210,7 @@ func NewTranscodingCache() TranscodingCache {
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)
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

View File

@@ -39,34 +39,34 @@ var _ = Describe("MediaStreamer", func() {
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())
})

View File

@@ -1,66 +0,0 @@
//go:build beep
package beepaudio
import (
"context"
"io"
"os"
"github.com/faiface/beep"
"github.com/faiface/beep/flac"
"github.com/faiface/beep/mp3"
"github.com/faiface/beep/wav"
"github.com/navidrome/navidrome/core/ffmpeg"
"github.com/navidrome/navidrome/log"
)
func DecodeMp3(path string) (s beep.StreamSeekCloser, format beep.Format, err error) {
f, err := os.Open(path)
if err != nil {
return nil, beep.Format{}, err
}
return mp3.Decode(f)
}
func DecodeWAV(path string) (s beep.StreamSeekCloser, format beep.Format, err error) {
f, err := os.Open(path)
if err != nil {
return nil, beep.Format{}, err
}
return wav.Decode(f)
}
func DecodeFLAC(path string) (s beep.StreamSeekCloser, format beep.Format, fileToCleanup string, err error) {
// TODO: Turn this into a semi-parallel operation: start playing while still transcoding/copying
log.Debug("decode to FLAC", "filename", path)
fFmpeg := ffmpeg.New()
readCloser, err := fFmpeg.ConvertToFLAC(context.TODO(), path)
if err != nil {
log.Error("error converting file to FLAC", path, err)
return nil, beep.Format{}, "", err
}
tempFile, err := os.CreateTemp("", "*.flac")
if err != nil {
log.Error("error creating temp file", err)
return nil, beep.Format{}, "", err
}
log.Debug("created tempfile", "filename", tempFile.Name())
written, err := io.Copy(tempFile, readCloser)
if err != nil {
log.Error("error coping file", "dest", tempFile.Name())
}
log.Debug("copy pipe into tempfile", "bytes written", written, "filename", tempFile.Name())
f, err := os.Open(tempFile.Name())
if err != nil {
log.Error("could not re-open tempfile", "filename", tempFile.Name())
return nil, beep.Format{}, "", err
}
s, format, err = flac.Decode(f)
return s, format, tempFile.Name(), err
}

View File

@@ -1,162 +0,0 @@
//go:build beep
package beepaudio
import (
"fmt"
"os"
"time"
"github.com/faiface/beep"
"github.com/faiface/beep/effects"
"github.com/faiface/beep/speaker"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
)
type BeepTrack struct {
MediaFile model.MediaFile
Ctrl *beep.Ctrl
Volume *effects.Volume
ActiveStream beep.StreamSeekCloser
TempfileToCleanup string
SampleRate beep.SampleRate
PlaybackDone chan bool
}
func NewTrack(playbackDoneChannel chan bool, mf model.MediaFile) (*BeepTrack, error) {
t := BeepTrack{}
contentType := mf.ContentType()
log.Debug("loading track", "trackname", mf.Path, "mediatype", contentType)
var streamer beep.StreamSeekCloser
var format beep.Format
var err error
var tmpfileToCleanup = ""
switch contentType {
case "audio/mpeg":
streamer, format, err = DecodeMp3(mf.Path)
case "audio/x-wav":
streamer, format, err = DecodeWAV(mf.Path)
case "audio/mp4":
streamer, format, tmpfileToCleanup, err = DecodeFLAC(mf.Path)
default:
return nil, fmt.Errorf("unsupported content type: %s", contentType)
}
if err != nil {
log.Error(err)
return nil, err
}
// save running stream for closing when switching tracks
t.ActiveStream = streamer
t.TempfileToCleanup = tmpfileToCleanup
log.Debug("Setting up audio device")
t.Ctrl = &beep.Ctrl{Streamer: streamer, Paused: true}
t.Volume = &effects.Volume{Streamer: t.Ctrl, Base: 2}
t.SampleRate = format.SampleRate
t.PlaybackDone = playbackDoneChannel
t.MediaFile = mf
err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
if err != nil {
log.Error(err)
}
log.Debug("speaker.Init() finished")
go func() {
speaker.Play(beep.Seq(t.Volume, beep.Callback(func() {
log.Info("Hitting end-of-stream, signalling on channel")
t.PlaybackDone <- true
log.Debug("Signalling finished")
})))
log.Debug("dropping out of speaker.Play()")
}()
return &t, nil
}
func (t *BeepTrack) String() string {
return fmt.Sprintf("Name: %s", t.MediaFile.Path)
}
func (t *BeepTrack) SetVolume(value float64) {
speaker.Lock()
t.Volume.Volume += value
speaker.Unlock()
}
func (t *BeepTrack) Unpause() {
speaker.Lock()
if t.Ctrl.Paused {
t.Ctrl.Paused = false
} else {
log.Debug("tried to unpause while not paused")
}
speaker.Unlock()
}
func (t *BeepTrack) Pause() {
speaker.Lock()
if t.Ctrl.Paused {
log.Debug("tried to pause while already paused")
} else {
t.Ctrl.Paused = true
}
speaker.Unlock()
}
func (t *BeepTrack) Close() {
if t.ActiveStream != nil {
log.Debug("closing activ stream")
t.ActiveStream.Close()
t.ActiveStream = nil
}
speaker.Close()
if t.TempfileToCleanup != "" {
log.Debug("Removing tempfile", "tmpfilename", t.TempfileToCleanup)
err := os.Remove(t.TempfileToCleanup)
if err != nil {
log.Error("error cleaning up tempfile: ", t.TempfileToCleanup)
}
}
}
// Position returns the playback position in seconds
func (t *BeepTrack) Position() int {
if t.Ctrl.Streamer == nil {
log.Debug("streamer is not setup (nil), could not get position")
return 0
}
streamer, ok := t.Ctrl.Streamer.(beep.StreamSeeker)
if ok {
position := t.SampleRate.D(streamer.Position())
posSecs := position.Round(time.Second).Seconds()
return int(posSecs)
} else {
log.Debug("streamer is no beep.StreamSeeker, could not get position")
return 0
}
}
// offset = pd.PlaybackQueue.Offset
func (t *BeepTrack) SetPosition(offset int) error {
streamer, ok := t.Ctrl.Streamer.(beep.StreamSeeker)
if ok {
sampleRatePerSecond := t.SampleRate.N(time.Second)
nextPosition := sampleRatePerSecond * offset
log.Debug("SetPosition", "samplerate", sampleRatePerSecond, "nextPosition", nextPosition)
return streamer.Seek(nextPosition)
}
return fmt.Errorf("streamer is not seekable")
}
func (t *BeepTrack) IsPlaying() bool {
return t.Ctrl != nil && !t.Ctrl.Paused
}

View File

@@ -2,7 +2,9 @@ package playback
import (
"context"
"errors"
"fmt"
"sync"
"github.com/navidrome/navidrome/core/playback/mpv"
"github.com/navidrome/navidrome/log"
@@ -17,9 +19,11 @@ type Track interface {
Position() int
SetPosition(offset int) error
Close()
String() string
}
type PlaybackDevice struct {
type playbackDevice struct {
serviceCtx context.Context
ParentPlaybackServer PlaybackServer
Default bool
User string
@@ -29,7 +33,7 @@ type PlaybackDevice struct {
Gain float32
PlaybackDone chan bool
ActiveTrack Track
TrackSwitcherStarted bool
startTrackSwitcher sync.Once
}
type DeviceStatus struct {
@@ -41,9 +45,7 @@ type DeviceStatus struct {
const DefaultGain float32 = 1.0
var EmptyStatus = DeviceStatus{CurrentIndex: -1, Playing: false, Gain: DefaultGain, Position: 0}
func (pd *PlaybackDevice) getStatus() DeviceStatus {
func (pd *playbackDevice) getStatus() DeviceStatus {
pos := 0
if pd.ActiveTrack != nil {
pos = pd.ActiveTrack.Position()
@@ -59,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 {
return &PlaybackDevice{
func NewPlaybackDevice(ctx context.Context, playbackServer PlaybackServer, name string, deviceName string) *playbackDevice {
return &playbackDevice{
serviceCtx: ctx,
ParentPlaybackServer: playbackServer,
User: "",
Name: name,
@@ -68,26 +71,27 @@ func NewPlaybackDevice(playbackServer PlaybackServer, name string, deviceName st
Gain: DefaultGain,
PlaybackQueue: NewQueue(),
PlaybackDone: make(chan bool),
TrackSwitcherStarted: false,
}
}
func (pd *PlaybackDevice) String() string {
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")
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) {
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) {
// 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)
@@ -96,17 +100,16 @@ func (pd *PlaybackDevice) Set(ctx context.Context, ids []string) (DeviceStatus,
return pd.Add(ctx, ids)
}
func (pd *PlaybackDevice) Start(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, "processing Start action")
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() {
@@ -127,16 +130,16 @@ func (pd *PlaybackDevice) Start(ctx context.Context) (DeviceStatus, error) {
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) Stop(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, "processing Stop action")
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)
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()
@@ -173,8 +176,11 @@ func (pd *PlaybackDevice) Skip(ctx context.Context, index int, offset int) (Devi
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) Add(ctx context.Context, ids []string) (DeviceStatus, error) {
log.Debug(ctx, "processing Add action")
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{}
@@ -191,8 +197,8 @@ func (pd *PlaybackDevice) Add(ctx context.Context, ids []string) (DeviceStatus,
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) Clear(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, fmt.Sprintf("processing Clear action on: %s", pd))
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()
@@ -202,8 +208,8 @@ func (pd *PlaybackDevice) Clear(ctx context.Context) (DeviceStatus, error) {
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) Remove(ctx context.Context, index int) (DeviceStatus, error) {
log.Debug(ctx, "processing Remove action")
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)
@@ -221,17 +227,17 @@ func (pd *PlaybackDevice) Remove(ctx context.Context, index int) (DeviceStatus,
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) Shuffle(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, "processing Shuffle action")
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
}
// 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, fmt.Sprintf("processing SetGain action. Actual gain: %f, gain to set: %f", pd.Gain, gain))
// 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)
@@ -241,45 +247,53 @@ func (pd *PlaybackDevice) SetGain(ctx context.Context, gain float32) (DeviceStat
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) isPlaying() bool {
func (pd *playbackDevice) isPlaying() bool {
return pd.ActiveTrack != nil && pd.ActiveTrack.IsPlaying()
}
func (pd *PlaybackDevice) trackSwitcherGoroutine() {
log.Info("Starting trackSwitcher goroutine")
func (pd *playbackDevice) trackSwitcherGoroutine() {
log.Debug("Started trackSwitcher goroutine", "device", pd)
for {
<-pd.PlaybackDone
log.Info("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", "error", 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
}
}
}
func (pd *PlaybackDevice) switchActiveTrackByIndex(index int) error {
func (pd *playbackDevice) switchActiveTrackByIndex(index int) error {
pd.PlaybackQueue.SetIndex(index)
currentTrack := pd.PlaybackQueue.Current()
if currentTrack == nil {
return fmt.Errorf("could not get current track")
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,15 +38,12 @@ 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.CurrentLevel() >= log.LevelTrace {
if log.IsGreaterOrEqualTo(log.LevelTrace) {
cmd.Stderr = os.Stderr
} else {
cmd.Stderr = io.Discard
@@ -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,11 +6,12 @@ package mpv
// https://mpv.io/manual/master/#properties
import (
"context"
"fmt"
"os"
"time"
"github.com/DexterLB/mpvipc"
"github.com/dexterlb/mpvipc"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
)
@@ -24,26 +25,26 @@ type MpvTrack struct {
CloseCalled bool
}
func NewTrack(playbackDoneChannel chan bool, deviceName string, mf model.MediaFile) (*MpvTrack, error) {
log.Debug("loading track", "trackname", mf.Path, "mediatype", mf.ContentType())
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", "error", err)
log.Error("Error starting mpv process", err)
return nil, err
}
// wait for socket to show up
err = waitForFile(tmpSocketName, 3*time.Second, 100*time.Millisecond)
err = waitForSocket(tmpSocketName, 3*time.Second, 100*time.Millisecond)
if err != nil {
log.Error("error or timeout waiting for control socket", "socketname", tmpSocketName, "error", err)
log.Error("Error or timeout waiting for control socket", "socketname", tmpSocketName, err)
return nil, err
}
@@ -51,7 +52,7 @@ func NewTrack(playbackDoneChannel chan bool, deviceName string, mf model.MediaFi
err = conn.Open()
if err != nil {
log.Error("error opening new connection", "error", err)
log.Error("Error opening new connection", err)
return nil, err
}
@@ -77,62 +78,57 @@ 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("request for gain", "gain", value)
log.Debug("Setting volume", "volume", value, "track", t)
vol := int(value * 100)
err := t.Conn.Set("volume", vol)
if err != nil {
log.Error(err)
log.Error("Error setting volume", "volume", value, "track", t, err)
}
log.Debug("set volume", "volume", vol)
}
func (t *MpvTrack) Unpause() {
log.Debug("Unpausing track", "track", t)
err := t.Conn.Set("pause", false)
if err != nil {
log.Error(err)
log.Error("Error unpausing track", "track", t, err)
}
log.Info("unpaused track")
}
func (t *MpvTrack) Pause() {
log.Debug("Pausing track", "track", t)
err := t.Conn.Set("pause", true)
if err != nil {
log.Error(err)
log.Error("Error pausing track", "track", t, err)
}
log.Info("paused track")
}
func (t *MpvTrack) Close() {
log.Debug("closing resources")
log.Debug("Closing resources", "track", t)
t.CloseCalled = true
// trying to shutdown mpv process using socket
if t.isSocketfilePresent() {
if t.isSocketFilePresent() {
log.Debug("sending shutdown command")
_, err := t.Conn.Call("quit")
if err != nil {
log.Error("error sending quit command to mpv-ipc socket", "error", 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")
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: ", t.IPCSocketName)
}
if t.isSocketFilePresent() {
removeSocket(t.IPCSocketName)
}
}
func (t *MpvTrack) isSocketfilePresent() bool {
func (t *MpvTrack) isSocketFilePresent() bool {
if len(t.IPCSocketName) < 1 {
return false
}
@@ -141,69 +137,70 @@ func (t *MpvTrack) isSocketfilePresent() bool {
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"
// 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" {
log.Debug("got the mpv error: property unavailable error, retry ...")
retryCount += 1
log.Debug("Got mpv error, retrying...", "retries", retryCount, err)
if retryCount > 5 {
return 0
}
break
time.Sleep(time.Duration(retryCount) * time.Millisecond)
continue
}
if err != nil {
log.Error("error getting position in track", "error", err)
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")
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")
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", "offset", offset, "error", err)
log.Error("Could not set the position in track", "track", t, "offset", offset, err)
return err
}
log.Info("set position", "offset", offset)
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", "error", err)
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")
log.Error("Could not cast pausing to boolean", "track", t, "value", pausing)
return false
}
return !pause
}
func waitForFile(path string, timeout time.Duration, pause time.Duration) error {
func waitForSocket(path string, timeout time.Duration, pause time.Duration) error {
start := time.Now()
end := start.Add(timeout)
var retries int = 0
@@ -211,7 +208,7 @@ func waitForFile(path string, timeout time.Duration, pause time.Duration) error
for {
fileInfo, err := os.Stat(path)
if err == nil && fileInfo != nil && !fileInfo.IsDir() {
log.Debug("file found", "retries", retries, "waittime", time.Since(start).Microseconds())
log.Debug("Socket found", "retries", retries, "waitTime", time.Since(start))
return nil
}
if time.Now().After(end) {

View File

@@ -1,5 +1,5 @@
// Package playback implements audio playback using PlaybackDevices. It is used to implement the Jukebox mode in turn.
// It makes use of the BEEP library to do the playback. Major parts are:
// 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
@@ -10,79 +10,71 @@ 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"
)
type PlaybackServer interface {
Run(ctx context.Context) error
GetDeviceForUser(user string) (*PlaybackDevice, error)
GetDeviceForUser(user string) (*playbackDevice, error)
GetMediaFile(id string) (*model.MediaFile, error)
GetCtx() *context.Context
}
type playbackServer struct {
ctx *context.Context
datastore model.DataStore
playbackDevices []PlaybackDevice
playbackDevices []playbackDevice
}
// 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) {
pbDevices := make([]PlaybackDevice, max(1, len(devices)))
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.
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]))
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 {
return []PlaybackDevice{}, fmt.Errorf("number of audio device found is %d, but no default device defined. Set Jukebox.Default", len(devices))
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
@@ -91,10 +83,10 @@ func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition,
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))
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
@@ -103,18 +95,18 @@ func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition,
}
if !defaultDeviceFound {
return []PlaybackDevice{}, fmt.Errorf("default device name not found: %s ", defaultDevice)
return []playbackDevice{}, fmt.Errorf("default device name not found: %s ", defaultDevice)
}
return pbDevices, nil
}
func (ps *playbackServer) getDefaultDevice() (*PlaybackDevice, error) {
for idx, audioDevice := range ps.playbackDevices {
if audioDevice.Default {
func (ps *playbackServer) getDefaultDevice() (*playbackDevice, error) {
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
@@ -123,12 +115,12 @@ func (ps *playbackServer) GetMediaFile(id string) (*model.MediaFile, error) {
}
// 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")
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 &PlaybackDevice{}, err
return nil, err
}
device.User = user
return device, nil

View File

@@ -104,7 +104,7 @@ func (pd *Queue) Shuffle() {
var err error
pd.Index, err = pd.getMediaFileIndexByID(backupID)
if err != nil {
log.Error("Could not find ID while shuffling: " + backupID)
log.Error("Could not find ID while shuffling: %s", backupID)
}
}
@@ -114,7 +114,7 @@ func (pd *Queue) getMediaFileIndexByID(id string) (int, error) {
return idx, nil
}
}
return -1, fmt.Errorf("ID not found in playlist: " + id)
return -1, fmt.Errorf("ID not found in playlist: %s", id)
}
// Sets the index to a new, valid value inside the Items. Values lower than zero are going to be zero,
@@ -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

@@ -13,7 +13,7 @@ import (
type Players interface {
Get(ctx context.Context, playerId string) (*model.Player, error)
Register(ctx context.Context, id, client, typ, ip string) (*model.Player, *model.Transcoding, error)
Register(ctx context.Context, id, client, userAgent, ip string) (*model.Player, *model.Transcoding, error)
}
func NewPlayers(ds model.DataStore) Players {
@@ -28,7 +28,7 @@ func (p *players) Register(ctx context.Context, id, client, userAgent, ip string
var plr *model.Player
var trc *model.Transcoding
var err error
userName, _ := request.UsernameFrom(ctx)
user, _ := request.UserFrom(ctx)
if id != "" {
plr, err = p.ds.Player(ctx).Get(id)
if err == nil && plr.Client != client {
@@ -36,22 +36,22 @@ func (p *players) Register(ctx context.Context, id, client, userAgent, ip string
}
}
if err != nil || id == "" {
plr, err = p.ds.Player(ctx).FindMatch(userName, client, userAgent)
plr, err = p.ds.Player(ctx).FindMatch(user.ID, client, userAgent)
if err == nil {
log.Debug(ctx, "Found matching player", "id", plr.ID, "client", client, "username", userName, "type", userAgent)
log.Debug(ctx, "Found matching player", "id", plr.ID, "client", client, "username", userName(ctx), "type", userAgent)
} else {
plr = &model.Player{
ID: uuid.NewString(),
UserName: userName,
UserId: user.ID,
Client: client,
ScrobbleEnabled: true,
}
log.Info(ctx, "Registering new player", "id", plr.ID, "client", client, "username", userName, "type", userAgent)
log.Info(ctx, "Registering new player", "id", plr.ID, "client", client, "username", userName(ctx), "type", userAgent)
}
}
plr.Name = fmt.Sprintf("%s [%s]", client, userAgent)
plr.UserAgent = userAgent
plr.IPAddress = ip
plr.IP = ip
plr.LastSeen = time.Now()
err = p.ds.Player(ctx).Put(plr)
if err != nil {

View File

@@ -34,7 +34,7 @@ var _ = Describe("Players", func() {
Expect(p.ID).ToNot(BeEmpty())
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
Expect(p.Client).To(Equal("client"))
Expect(p.UserName).To(Equal("johndoe"))
Expect(p.UserId).To(Equal("userid"))
Expect(p.UserAgent).To(Equal("chrome"))
Expect(repo.lastSaved).To(Equal(p))
Expect(trc).To(BeNil())
@@ -73,7 +73,7 @@ var _ = Describe("Players", func() {
})
It("finds player by client and user names when ID is not found", func() {
plr := &model.Player{ID: "123", Name: "A Player", Client: "client", UserName: "johndoe", LastSeen: time.Time{}}
plr := &model.Player{ID: "123", Name: "A Player", Client: "client", UserId: "userid", UserAgent: "chrome", LastSeen: time.Time{}}
repo.add(plr)
p, _, err := players.Register(ctx, "999", "client", "chrome", "1.2.3.4")
Expect(err).ToNot(HaveOccurred())
@@ -83,7 +83,7 @@ var _ = Describe("Players", func() {
})
It("finds player by client and user names when not ID is provided", func() {
plr := &model.Player{ID: "123", Name: "A Player", Client: "client", UserName: "johndoe", LastSeen: time.Time{}}
plr := &model.Player{ID: "123", Name: "A Player", Client: "client", UserId: "userid", UserAgent: "chrome", LastSeen: time.Time{}}
repo.add(plr)
p, _, err := players.Register(ctx, "", "client", "chrome", "1.2.3.4")
Expect(err).ToNot(HaveOccurred())
@@ -102,6 +102,22 @@ var _ = Describe("Players", func() {
Expect(repo.lastSaved).To(Equal(p))
Expect(trc.ID).To(Equal("1"))
})
Context("bad username casing", func() {
ctx := log.NewContext(context.TODO())
ctx = request.WithUser(ctx, model.User{ID: "userid", UserName: "Johndoe"})
ctx = request.WithUsername(ctx, "Johndoe")
It("finds player by client and user names when not ID is provided", func() {
plr := &model.Player{ID: "123", Name: "A Player", Client: "client", UserId: "userid", UserAgent: "chrome", LastSeen: time.Time{}}
repo.add(plr)
p, _, err := players.Register(ctx, "", "client", "chrome", "1.2.3.4")
Expect(err).ToNot(HaveOccurred())
Expect(p.ID).To(Equal("123"))
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
Expect(repo.lastSaved).To(Equal(p))
})
})
})
})
@@ -125,9 +141,9 @@ func (m *mockPlayerRepository) Get(id string) (*model.Player, error) {
return nil, model.ErrNotFound
}
func (m *mockPlayerRepository) FindMatch(userName, client, typ string) (*model.Player, error) {
func (m *mockPlayerRepository) FindMatch(userId, client, userAgent string) (*model.Player, error) {
for _, p := range m.data {
if p.Client == client && p.UserName == userName {
if p.Client == client && p.UserId == userId && p.UserAgent == userAgent {
return &p, nil
}
}

View File

@@ -14,6 +14,8 @@ import (
"strings"
"time"
"github.com/RaveNoX/go-jsoncommentstrip"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/criteria"
@@ -112,7 +114,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)
@@ -187,10 +190,11 @@ 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
newPls.Public = conf.Server.DefaultPlaylistPublicVisibility
}
return s.ds.Playlist(ctx).Put(newPls)
}
@@ -229,13 +233,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
}
@@ -262,7 +270,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

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

@@ -7,17 +7,14 @@ import (
"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"
"github.com/navidrome/navidrome/server/events"
"github.com/navidrome/navidrome/utils/cache"
"github.com/navidrome/navidrome/utils/singleton"
)
const maxNowPlayingExpire = 60 * time.Minute
type NowPlayingInfo struct {
MediaFile model.MediaFile
Start time.Time
@@ -40,7 +37,7 @@ type PlayTracker interface {
type playTracker struct {
ds model.DataStore
broker events.Broker
playMap *ttlcache.Cache
playMap cache.SimpleCache[string, NowPlayingInfo]
scrobblers map[string]Scrobbler
}
@@ -53,9 +50,7 @@ func GetPlayTracker(ds model.DataStore, broker events.Broker) PlayTracker {
// This constructor only exists for testing. For normal usage, the PlayTracker has to be a singleton, returned by
// the GetPlayTracker function above
func newPlayTracker(ds model.DataStore, broker events.Broker) *playTracker {
m := ttlcache.NewCache()
m.SkipTTLExtensionOnHit(true)
_ = m.SetTTL(maxNowPlayingExpire)
m := cache.NewSimpleCache[string, NowPlayingInfo]()
p := &playTracker{ds: ds, playMap: m, broker: broker}
p.scrobblers = make(map[string]Scrobbler)
for name, constructor := range constructors {
@@ -85,7 +80,7 @@ func (p *playTracker) NowPlaying(ctx context.Context, playerId string, playerNam
}
ttl := time.Duration(int(mf.Duration)+5) * time.Second
_ = p.playMap.SetWithTTL(playerId, info, ttl)
_ = p.playMap.AddWithTTL(playerId, info, ttl)
player, _ := request.PlayerFrom(ctx)
if player.ScrobbleEnabled {
p.dispatchNowPlaying(ctx, user.ID, mf)
@@ -112,15 +107,7 @@ func (p *playTracker) dispatchNowPlaying(ctx context.Context, userId string, t *
}
func (p *playTracker) GetNowPlaying(_ context.Context) ([]NowPlayingInfo, error) {
var res []NowPlayingInfo
for _, playerId := range p.playMap.GetKeys() {
value, err := p.playMap.Get(playerId)
if err != nil {
continue
}
info := value.(NowPlayingInfo)
res = append(res, info)
}
res := p.playMap.Values()
sort.Slice(res, func(i, j int) bool {
return res[i].Start.After(res[j].Start)
})
@@ -131,7 +118,7 @@ func (p *playTracker) Submit(ctx context.Context, submissions []Submission) erro
username, _ := request.UsernameFrom(ctx)
player, _ := request.PlayerFrom(ctx)
if !player.ScrobbleEnabled {
log.Debug(ctx, "External scrobbling disabled for this player", "player", player.Name, "ip", player.IPAddress, "user", username)
log.Debug(ctx, "External scrobbling disabled for this player", "player", player.Name, "ip", player.IP, "user", username)
}
event := &events.RefreshResource{}
success := 0
@@ -163,15 +150,15 @@ func (p *playTracker) Submit(ctx context.Context, submissions []Submission) erro
func (p *playTracker) incPlay(ctx context.Context, track *model.MediaFile, timestamp time.Time) error {
return p.ds.WithTx(func(tx model.DataStore) error {
err := p.ds.MediaFile(ctx).IncPlayCount(track.ID, timestamp)
err := tx.MediaFile(ctx).IncPlayCount(track.ID, timestamp)
if err != nil {
return err
}
err = p.ds.Album(ctx).IncPlayCount(track.AlbumID, timestamp)
err = tx.Album(ctx).IncPlayCount(track.AlbumID, timestamp)
if err != nil {
return err
}
err = p.ds.Artist(ctx).IncPlayCount(track.ArtistID, timestamp)
err = tx.Artist(ctx).IncPlayCount(track.ArtistID, timestamp)
return err
})
}

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

107
db/db.go
View File

@@ -4,11 +4,13 @@ import (
"database/sql"
"embed"
"fmt"
"runtime"
_ "github.com/mattn/go-sqlite3"
"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/hasher"
"github.com/navidrome/navidrome/utils/singleton"
"github.com/pressly/goose/v3"
)
@@ -18,34 +20,82 @@ var (
Path string
)
//go:embed migration/*.sql
//go:embed migrations/*.sql
var embedMigrations embed.FS
const migrationsFolder = "migration"
const migrationsFolder = "migrations"
type DB interface {
ReadDB() *sql.DB
WriteDB() *sql.DB
Close()
}
type db struct {
readDB *sql.DB
writeDB *sql.DB
}
func (d *db) ReadDB() *sql.DB {
return d.readDB
}
func (d *db) WriteDB() *sql.DB {
return d.writeDB
}
func (d *db) Close() {
if err := d.readDB.Close(); err != nil {
log.Error("Error closing read DB", err)
}
if err := d.writeDB.Close(); err != nil {
log.Error("Error closing write DB", err)
}
}
func Db() DB {
return singleton.GetInstance(func() *db {
sql.Register(Driver+"_custom", &sqlite3.SQLiteDriver{
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
return conn.RegisterFunc("SEEDEDRAND", hasher.HashFunc(), false)
},
})
func Db() *sql.DB {
return singleton.GetInstance(func() *sql.DB {
Path = conf.Server.DbPath
if Path == ":memory:" {
Path = "file::memory:?cache=shared&_foreign_keys=on"
conf.Server.DbPath = Path
}
log.Debug("Opening DataBase", "dbPath", Path, "driver", Driver)
instance, err := sql.Open(Driver, Path)
// Create a read database connection
rdb, err := sql.Open(Driver+"_custom", Path)
if err != nil {
panic(err)
log.Fatal("Error opening read database", err)
}
rdb.SetMaxOpenConns(max(4, runtime.NumCPU()))
// Create a write database connection
wdb, err := sql.Open(Driver+"_custom", Path)
if err != nil {
log.Fatal("Error opening write database", err)
}
wdb.SetMaxOpenConns(1)
return &db{
readDB: rdb,
writeDB: wdb,
}
return instance
})
}
func Close() error {
func Close() {
log.Info("Closing Database")
return Db().Close()
Db().Close()
}
func Init() {
db := Db()
func Init() func() {
db := Db().WriteDB()
// Disable foreign_keys to allow re-creating tables in migrations
_, err := db.Exec("PRAGMA foreign_keys=off")
@@ -60,17 +110,46 @@ 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 Close
}
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

@@ -1,20 +0,0 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose/v3"
)
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/v3"
)
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,23 +0,0 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upAddDownloadToShare, downAddDownloadToShare)
}
func upAddDownloadToShare(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table share
add downloadable bool not null default false;
`)
return err
}
func downAddDownloadToShare(tx *sql.Tx) error {
return nil
}

View File

@@ -1,6 +1,7 @@
package migrations
import (
"context"
"database/sql"
"github.com/navidrome/navidrome/log"
@@ -8,10 +9,10 @@ import (
)
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/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/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/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

@@ -1,20 +1,21 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200310171621, Down20200310171621)
goose.AddMigrationContext(Up20200310171621, Down20200310171621)
}
func Up20200310171621(tx *sql.Tx) error {
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(tx *sql.Tx) error {
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/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/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/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/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
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200404214704, Down20200404214704)
goose.AddMigrationContext(Up20200404214704, Down20200404214704)
}
func Up20200404214704(tx *sql.Tx) error {
func Up20200404214704(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create index if not exists media_file_year
on media_file (year);
@@ -24,6 +25,6 @@ create index if not exists media_file_track_number
return err
}
func Down20200404214704(tx *sql.Tx) error {
func Down20200404214704(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,20 +1,21 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200409002249, Down20200409002249)
goose.AddMigrationContext(Up20200409002249, Down20200409002249)
}
func Up20200409002249(tx *sql.Tx) error {
func Up20200409002249(_ context.Context, 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 {
func Down20200409002249(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200411164603, Down20200411164603)
goose.AddMigrationContext(Up20200411164603, Down20200411164603)
}
func Up20200411164603(tx *sql.Tx) error {
func Up20200411164603(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table playlist
add created_at datetime;
@@ -22,6 +23,6 @@ update playlist
return err
}
func Down20200411164603(tx *sql.Tx) error {
func Down20200411164603(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,20 +1,21 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200418110522, Down20200418110522)
goose.AddMigrationContext(Up20200418110522, Down20200418110522)
}
func Up20200418110522(tx *sql.Tx) error {
func Up20200418110522(_ context.Context, 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 {
func Down20200418110522(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,20 +1,21 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200419222708, Down20200419222708)
goose.AddMigrationContext(Up20200419222708, Down20200419222708)
}
func Up20200419222708(tx *sql.Tx) error {
func Up20200419222708(_ context.Context, 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 {
func Down20200419222708(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200423204116, Down20200423204116)
goose.AddMigrationContext(Up20200423204116, Down20200423204116)
}
func Up20200423204116(tx *sql.Tx) error {
func Up20200423204116(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table artist
add order_artist_name varchar(255) collate nocase;
@@ -60,6 +61,6 @@ create index if not exists media_file_order_artist_name
return forceFullRescan(tx)
}
func Down20200423204116(tx *sql.Tx) error {
func Down20200423204116(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200508093059, Down20200508093059)
goose.AddMigrationContext(Up20200508093059, Down20200508093059)
}
func Up20200508093059(tx *sql.Tx) error {
func Up20200508093059(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table artist
add song_count integer default 0 not null;
@@ -22,6 +23,6 @@ alter table artist
return forceFullRescan(tx)
}
func Down20200508093059(tx *sql.Tx) error {
func Down20200508093059(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200512104202, Down20200512104202)
goose.AddMigrationContext(Up20200512104202, Down20200512104202)
}
func Up20200512104202(tx *sql.Tx) error {
func Up20200512104202(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table media_file
add disc_subtitle varchar(255);
@@ -22,6 +23,6 @@ alter table media_file
return forceFullRescan(tx)
}
func Down20200512104202(tx *sql.Tx) error {
func Down20200512104202(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,6 +1,7 @@
package migrations
import (
"context"
"database/sql"
"strings"
@@ -9,10 +10,10 @@ import (
)
func init() {
goose.AddMigration(Up20200516140647, Down20200516140647)
goose.AddMigrationContext(Up20200516140647, Down20200516140647)
}
func Up20200516140647(tx *sql.Tx) error {
func Up20200516140647(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table if not exists playlist_tracks
(
@@ -95,6 +96,6 @@ func Up20200516140647UpdatePlaylistTracks(tx *sql.Tx, id string, tracks string)
return nil
}
func Down20200516140647(tx *sql.Tx) error {
func Down20200516140647(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20200608153717, Down20200608153717)
goose.AddMigrationContext(Up20200608153717, Down20200608153717)
}
func Up20200608153717(tx *sql.Tx) error {
func Up20200608153717(_ context.Context, tx *sql.Tx) error {
// First delete dangling players
_, err := tx.Exec(`
delete from player where user_name not in (select user_name from user)`)
@@ -132,6 +133,6 @@ create unique index playlist_tracks_pos
return err
}
func Down20200608153717(tx *sql.Tx) error {
func Down20200608153717(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,6 +1,7 @@
package migrations
import (
"context"
"database/sql"
"github.com/google/uuid"
@@ -9,10 +10,10 @@ import (
)
func init() {
goose.AddMigration(upAddDefaultTranscodings, downAddDefaultTranscodings)
goose.AddMigrationContext(upAddDefaultTranscodings, downAddDefaultTranscodings)
}
func upAddDefaultTranscodings(tx *sql.Tx) error {
func upAddDefaultTranscodings(_ context.Context, tx *sql.Tx) error {
row := tx.QueryRow("SELECT COUNT(*) FROM transcoding")
var count int
err := row.Scan(&count)
@@ -29,7 +30,7 @@ func upAddDefaultTranscodings(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
}
@@ -37,6 +38,6 @@ func upAddDefaultTranscodings(tx *sql.Tx) error {
return nil
}
func downAddDefaultTranscodings(tx *sql.Tx) error {
func downAddDefaultTranscodings(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upAddPlaylistPath, downAddPlaylistPath)
goose.AddMigrationContext(upAddPlaylistPath, downAddPlaylistPath)
}
func upAddPlaylistPath(tx *sql.Tx) error {
func upAddPlaylistPath(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table playlist
add path string default '' not null;
@@ -22,6 +23,6 @@ alter table playlist
return err
}
func downAddPlaylistPath(tx *sql.Tx) error {
func downAddPlaylistPath(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upCreatePlayQueuesTable, downCreatePlayQueuesTable)
goose.AddMigrationContext(upCreatePlayQueuesTable, downCreatePlayQueuesTable)
}
func upCreatePlayQueuesTable(tx *sql.Tx) error {
func upCreatePlayQueuesTable(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table playqueue
(
@@ -31,6 +32,6 @@ create table playqueue
return err
}
func downCreatePlayQueuesTable(tx *sql.Tx) error {
func downCreatePlayQueuesTable(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upCreateBookmarkTable, downCreateBookmarkTable)
goose.AddMigrationContext(upCreateBookmarkTable, downCreateBookmarkTable)
}
func upCreateBookmarkTable(tx *sql.Tx) error {
func upCreateBookmarkTable(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table bookmark
(
@@ -48,6 +49,6 @@ alter table playqueue_dg_tmp rename to playqueue;
return err
}
func downCreateBookmarkTable(tx *sql.Tx) error {
func downCreateBookmarkTable(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upDropEmailUniqueConstraint, downDropEmailUniqueConstraint)
goose.AddMigrationContext(upDropEmailUniqueConstraint, downDropEmailUniqueConstraint)
}
func upDropEmailUniqueConstraint(tx *sql.Tx) error {
func upDropEmailUniqueConstraint(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create table user_dg_tmp
(
@@ -37,6 +38,6 @@ alter table user_dg_tmp rename to user;
return err
}
func downDropEmailUniqueConstraint(tx *sql.Tx) error {
func downDropEmailUniqueConstraint(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

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

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20201010162350, Down20201010162350)
goose.AddMigrationContext(Up20201010162350, Down20201010162350)
}
func Up20201010162350(tx *sql.Tx) error {
func Up20201010162350(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table album
add size integer default 0 not null;
@@ -27,7 +28,7 @@ where id not null;`)
return err
}
func Down20201010162350(tx *sql.Tx) error {
func Down20201010162350(_ context.Context, tx *sql.Tx) error {
// This code is executed when the migration is rolled back.
return nil
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20201012210022, Down20201012210022)
goose.AddMigrationContext(Up20201012210022, Down20201012210022)
}
func Up20201012210022(tx *sql.Tx) error {
func Up20201012210022(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table artist
add size integer default 0 not null;
@@ -39,6 +40,6 @@ update playlist set size = ifnull((
return err
}
func Down20201012210022(tx *sql.Tx) error {
func Down20201012210022(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20201021085410, Down20201021085410)
goose.AddMigrationContext(Up20201021085410, Down20201021085410)
}
func Up20201021085410(tx *sql.Tx) error {
func Up20201021085410(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table media_file
add mbz_track_id varchar(255);
@@ -52,7 +53,7 @@ alter table artist
return forceFullRescan(tx)
}
func Down20201021085410(tx *sql.Tx) error {
func Down20201021085410(_ context.Context, tx *sql.Tx) error {
// This code is executed when the migration is rolled back.
return nil
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20201021093209, Down20201021093209)
goose.AddMigrationContext(Up20201021093209, Down20201021093209)
}
func Up20201021093209(tx *sql.Tx) error {
func Up20201021093209(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
create index if not exists media_file_artist
on media_file (artist);
@@ -22,6 +23,6 @@ create index if not exists media_file_mbz_track_id
return err
}
func Down20201021093209(tx *sql.Tx) error {
func Down20201021093209(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

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

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upAddArtistImageUrl, downAddArtistImageUrl)
goose.AddMigrationContext(upAddArtistImageUrl, downAddArtistImageUrl)
}
func upAddArtistImageUrl(tx *sql.Tx) error {
func upAddArtistImageUrl(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table artist
add biography varchar(255) default '' not null;
@@ -30,6 +31,6 @@ alter table artist
return err
}
func downAddArtistImageUrl(tx *sql.Tx) error {
func downAddArtistImageUrl(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

@@ -1,16 +1,17 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20201110205344, Down20201110205344)
goose.AddMigrationContext(Up20201110205344, Down20201110205344)
}
func Up20201110205344(tx *sql.Tx) error {
func Up20201110205344(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table media_file
add comment varchar;
@@ -27,6 +28,6 @@ alter table album
return forceFullRescan(tx)
}
func Down20201110205344(tx *sql.Tx) error {
func Down20201110205344(_ context.Context, tx *sql.Tx) error {
return nil
}

View File

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

View File

@@ -1,18 +1,19 @@
package migrations
import (
"context"
"database/sql"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/str"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up20201213124814, Down20201213124814)
goose.AddMigrationContext(Up20201213124814, Down20201213124814)
}
func Up20201213124814(tx *sql.Tx) error {
func Up20201213124814(_ context.Context, tx *sql.Tx) error {
_, err := tx.Exec(`
alter table album
add all_artist_ids varchar;
@@ -49,7 +50,7 @@ select a.id, a.name, a.artist_id, a.album_artist_id, group_concat(mf.artist_id,
if err != nil {
return err
}
all := utils.SanitizeStrings(artistId, albumArtistId, songArtistIds.String)
all := str.SanitizeStrings(artistId, albumArtistId, songArtistIds.String)
_, err = stmt.Exec(all, id)
if err != nil {
log.Error("Error setting album's artist_ids", "album", name, "albumId", id, err)
@@ -58,6 +59,6 @@ select a.id, a.name, a.artist_id, a.album_artist_id, group_concat(mf.artist_id,
return rows.Err()
}
func Down20201213124814(tx *sql.Tx) error {
func Down20201213124814(_ context.Context, tx *sql.Tx) error {
return nil
}

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