Compare commits

...

1229 Commits

Author SHA1 Message Date
Deluan
8bbb878bb3 Add Finnish translation 2021-10-06 18:11:02 -04:00
Deluan Quintão
8591a9acdf Update translations (#1383)
* Add Persian translation

* Update cs.json (POEditor.com)

* Update fr.json (POEditor.com)

* Update ja.json (POEditor.com)

* Update pt.json (POEditor.com)

* Update ru.json (POEditor.com)

* Update sl.json (POEditor.com)

* Fix "Shared Playlists" pt translation

* Update de.json (POEditor.com)

* Update it.json (POEditor.com)

* Update sl.json (POEditor.com)

* Update es.json (POEditor.com)

* Update zh-Hant.json (POEditor.com)

* Update th.json (POEditor.com)
2021-10-06 18:08:29 -04:00
Deluan
b6e30cd01f Return playlists sorted in getPlaylists Subsonic endpoint 2021-10-05 14:47:57 -04:00
Deluan Quintão
058e7e4374 Add logo to README 2021-10-05 12:15:44 -04:00
Deluan
152836a01c Bump @testing-library/react from 12.1.1 to 12.1.2 in /ui 2021-10-04 17:30:01 -04:00
Deluan
6139338e80 Bump react-icons from 4.2.0 to 4.3.1 in /ui 2021-10-04 17:29:27 -04:00
Deluan
f0c11916ca Revert: Small optimization in genre mapping 2021-10-04 17:28:45 -04:00
Dnouv
a6311259fd Fix layout error in ArtistShow (#1387) 2021-10-04 17:14:38 -04:00
Deluan
dbde0ffa0c Bump github.com/djherbis/atime to v1.1.0 2021-10-03 22:50:25 -04:00
Deluan
fba733708c Sort songs by artist/album/disc/track_number before adding to playlist 2021-10-02 21:55:45 -04:00
Deluan
e673360087 Limit number of playlists displayed in the sidebar, to avoid UI freezes 2021-10-02 21:39:33 -04:00
Deluan
2b105ca77b Enable DevSidebarPlaylists by default.
Closes #771
2021-10-02 13:56:42 -04:00
Deluan
9c29ee3651 Check permissions before adding songs to playlists 2021-10-02 13:23:17 -04:00
Deluan
6c3e45de41 Add songs to playlists with drag and drop 2021-10-02 13:14:33 -04:00
dependabot[bot]
2ab4647420 Bump golang.org/x/tools from 0.1.6 to 0.1.7 (#1382)
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.1.6 to 0.1.7.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.1.6...v0.1.7)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  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>
2021-10-01 16:38:04 -04:00
Deluan Quintão
91e36a2c18 Check goimports in the pipeline (#1381)
* Check goimports in the pipeline

* Check goimports in the pipeline

* Check goimports in the pipeline

* go mod tidy

* wip

* wip

* Fix goimports and go:build tags

* Run golangci-lint before goimports
2021-10-01 15:32:24 -04:00
dependabot[bot]
0cbba80284 Bump react-router-dom from 5.2.0 to 5.3.0 in /ui (#1330)
Bumps [react-router-dom](https://github.com/ReactTraining/react-router) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/ReactTraining/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ReactTraining/react-router/compare/v5.2.0...v5.3.0)

---
updated-dependencies:
- dependency-name: react-router-dom
  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>
2021-10-01 10:34:37 -04:00
Deluan
f9d910473f Bump react-admin to 3.18.3 2021-10-01 09:45:28 -04:00
Igor Rzegocki
be3a6dc7a3 Use local copy of workbox service worker scripts (#1358)
* Use local copy of workbox service worker scripts

* Refactor workbox integration:

- Only add prod js, without maps. Reduces the size from 170k to 24k
- Removed it from build. As it is small now, we can add it to source, and have a script to just update it whenever it is required
- Fixed relative paths in navidrome-service-worker.js, should now work with BaseUrl != ''

Co-authored-by: Deluan <deluan@navidrome.org>
2021-10-01 09:14:15 -04:00
Deluan
b1e7760996 Preload next song 2021-10-01 08:43:59 -04:00
Deluan
ad45ab4a04 Fix genre update chunking 2021-10-01 08:43:59 -04:00
Serguey Parkhomovsky
24fef584ad Bump react-jinke-music-player from 4.24.0 to 4.24.2 (#1378)
This should fix #1367.
2021-10-01 08:42:02 -04:00
Deluan
e17d436902 Do not attach Genres to the "Various Artists" artist 2021-09-27 21:55:33 -04:00
Deluan
73659e5669 Change "Build" link to point to the latest build artifacts from master 2021-09-27 18:17:53 -04:00
Deluan
71b1e7f870 Bump github.com/lestrrat-go/jwx from 1.2.6 to 1.2.7 2021-09-27 13:29:31 -04:00
Deluan
694056010c Bump @testing-library/react from 12.1.0 to 12.1.1 in /ui 2021-09-27 13:27:59 -04:00
Deluan
4fda895a64 Bump blueimp-md5 from 2.18.0 to 2.19.0 in /ui 2021-09-27 13:26:31 -04:00
Deluan
f664af5559 Bump react-admin from 3.18.1 to 3.18.2 in /ui 2021-09-27 13:26:31 -04:00
Deluan
c6868ff8a0 Don't show Artist Page for "Various Artists" 2021-09-27 11:52:23 -04:00
Deluan
0b65a4e34e Fix comment word wrapping 2021-09-27 09:48:31 -04:00
Deluan
24872e6c2a Fix biography word wrapping and requests for undefined resource 2021-09-27 09:47:16 -04:00
Deluan
b4e5c662dc Fix JS console warning 2021-09-26 17:36:30 -04:00
Deluan
6752e0a17d Fix harmless error message in logs when ScanSchedule set was "0"
Message:
`ERRO[0000] Error scheduling periodic scan                error="expected exactly 5 fields, found 1: [0]"`
2021-09-26 15:57:27 -04:00
Deluan
5680e53949 Update genres in chunks. Should fix #1368 2021-09-26 15:55:52 -04:00
Dnouv
482c2dec0c Artist Detail Page (first cut) (#1287)
* Configure fetching from API and route

* pretty

* Remove errors

* Remove errors

* Remove errors

* Complete page for Desktop view

* Fix error

* Add xs Artist page

* Remove unused import

* Add styles for theme

* Change route path

* Remove artId useEffect array

* Remove array

* Fix cover load err

* Add redirect on err

* Remove route

* What's in a name? consistency :)

* Fix err

* Fix UI changes

* Fetch album from resource

* Renaming done

* Review changes

* Some touch-up

* Small refactor, to make naming and structure more consistent with AlbumShow

* Make artist's album list similar to original implementation

* Reuse AlbumGridView, to avoid duplication

* Add feature flag to enable new Artist Page, default false

* Better biography styling. Small refactorings,

* Don't encode quotes and other symbols

* Moved AlbumShow to correct folder

Co-authored-by: Deluan <deluan@navidrome.org>
2021-09-26 15:32:40 -04:00
caiocotts
210dc6b12e Add x-total-count to Subsonic API getAlbumList (#1360)
* Add x-total-count to Subsonic API getAlbumList

* Rename variable

Co-authored-by: Deluan <deluan@navidrome.org>
2021-09-21 20:40:39 -04:00
Deluan
73a2271cdd Small optimization in genre mapping 2021-09-21 13:37:44 -04:00
Samarjeet
0c0bd2967d Replace expanded with a dialog (#1258)
* Replace expanded with a dialog

* Change `info` label to "Get Info"

* Rename things for consistency

Co-authored-by: Deluan <deluan@navidrome.org>
2021-09-20 20:30:43 -04:00
Deluan
15ae3d47cf Only apply audioStreamRx once 2021-09-20 19:33:50 -04:00
Deluan
1365adbb39 Support 7.1 (8) channels 2021-09-20 19:33:50 -04:00
Miguel A. Arroyo
e12a14a87d feat: Adds Audio Channel Metadata - #1036 2021-09-20 19:33:50 -04:00
Deluan
0079a9b938 Close Sidebar when going to Playlists list 2021-09-20 19:00:37 -04:00
Deluan
892c2bfd58 Revert "Disable mini-player (bubble) dragging. Should fix #1217"
This reverts commit abf6318a8b.
2021-09-20 18:24:18 -04:00
dependabot[bot]
5fb7328965 Bump golang.org/x/tools from 0.1.5 to 0.1.6
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.1.5 to 0.1.6.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.1.5...v0.1.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-20 18:18:45 -04:00
Deluan
86479a6d06 More info when recovering from panic 2021-09-20 18:16:22 -04:00
Deluan
76bd20e8ff Recover from any possible taglib panics. Fixes #1343 2021-09-20 18:09:39 -04:00
dependabot[bot]
7a15ed0740 Bump prettier from 2.4.0 to 2.4.1 in /ui
Bumps [prettier](https://github.com/prettier/prettier) from 2.4.0 to 2.4.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.4.0...2.4.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-20 17:45:50 -04:00
dependabot[bot]
d574df5fd7 Bump github.com/go-chi/jwtauth/v5 from 5.0.1 to 5.0.2
Bumps [github.com/go-chi/jwtauth/v5](https://github.com/go-chi/jwtauth) from 5.0.1 to 5.0.2.
- [Release notes](https://github.com/go-chi/jwtauth/releases)
- [Commits](https://github.com/go-chi/jwtauth/compare/v5.0.1...v5.0.2)

---
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>
2021-09-20 17:45:25 -04:00
dependabot[bot]
a7ace48efb Bump github.com/ReneKroon/ttlcache/v2 from 2.8.0 to 2.8.1
Bumps [github.com/ReneKroon/ttlcache/v2](https://github.com/ReneKroon/ttlcache) from 2.8.0 to 2.8.1.
- [Release notes](https://github.com/ReneKroon/ttlcache/releases)
- [Changelog](https://github.com/ReneKroon/ttlcache/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ReneKroon/ttlcache/compare/v2.8.0...v2.8.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-20 17:44:52 -04:00
dependabot[bot]
84d98010d3 Bump github.com/spf13/viper from 1.8.1 to 1.9.0
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.8.1 to 1.9.0.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.8.1...v1.9.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>
2021-09-20 17:44:02 -04:00
Deluan
e63804fbdd Use newer versions of node in the pipeline 2021-09-18 22:38:55 -04:00
Deluan
a5101fa9b4 Use npm dependencies cache from setup-node@v2 2021-09-18 22:38:55 -04:00
dependabot[bot]
f2ed3f2d86 Bump prettier from 2.3.2 to 2.4.0 in /ui (#1341)
Bumps [prettier](https://github.com/prettier/prettier) from 2.3.2 to 2.4.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/2.3.2...2.4.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>
2021-09-13 16:12:29 -04:00
dependabot[bot]
b23ab1ccec Bump @testing-library/react from 12.0.0 to 12.1.0 in /ui (#1342)
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 12.0.0 to 12.1.0.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v12.0.0...v12.1.0)

---
updated-dependencies:
- dependency-name: "@testing-library/react"
  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>
2021-09-13 16:10:07 -04:00
Deluan
abf6318a8b Disable mini-player (bubble) dragging. Should fix #1217 2021-09-13 11:17:06 -04:00
Deluan
b55f3a6946 Add paddingBottom to the whole sidebar menu, to avoid playlists to be covered by the player 2021-09-12 21:24:07 -04:00
Deluan
ab2912b4fa Only import playlists from configured paths in option PlaylistsPath. Closes #1181
Syntax is Ant-style Globs, with support for '**' (any subfolder). Default: '.:**' (or '.;**' in Windows`, meaning all folders and subfolders under `MusicFolder`
2021-09-12 21:07:51 -04:00
Deluan
9f00aad216 Upgrade to GoLang 1.17.1 2021-09-12 14:14:33 -04:00
Deluan Quintão
79363d6c07 Move Playlists to the sidebar menu (#1339)
* Show playlists in sidebar menu

* Fix menu

* Refresh playlist submenu when adding new playlist

* Group shared playlists below user's playlists

* Fix text overflow in menu options

* Add button in playlist menu to go to Playlists list

* Add config option `DevSidebarPlaylists` to enable this feature (default false)
2021-09-11 13:11:15 -04:00
Deluan
a7017e4bb0 Fix JS console warning 2021-09-10 18:04:38 -04:00
Deluan
dc0ec32dbf Fix menu items highlight 2021-09-10 16:17:08 -04:00
Salman Inayat
06b1a1a25c Album size overflow fixed (#1071)
* Added back button

* Added back button

* Added back button

* Fixed Album size overflow

* Fixed Album size overflow

* Fixed album size overflowing

* Fixed album size overflowing

* Fixed album size overflowing

* Fixed album size overflow on small screen

* Changes reverted in PlayerEdit.js

* prettier formatting issue resolved

Co-authored-by: Deluan <deluan@navidrome.org>
2021-09-09 11:56:48 -04:00
Deluan
6ac2fefaf3 Make AppBar stick on scroll 2021-09-09 11:21:16 -04:00
Samarjeet
2e921cd793 Fix sidebar scroll height (#1338)
* Fix sidebar scroll height

* Prettier

Co-authored-by: Deluan <deluan@navidrome.org>
2021-09-09 09:33:38 -04:00
Deluan Quintão
c2251e6ddc Update GoLang to 1.17 (#1295)
* Update GoLang to 1.17

* Rename pipeline jobs
2021-09-09 00:26:56 -04:00
Deluan
25aab8bcb5 go mod tidy 2021-09-09 00:17:43 -04:00
Deluan
94083f85d2 Bump @testing-library/react-hooks version 2021-09-09 00:16:44 -04:00
Deluan
f0ef5187b2 Bump react-redux version 2021-09-09 00:16:13 -04:00
Deluan
79e79b6366 Bump github.com/go-chi/chi 2021-09-09 00:15:16 -04:00
Deluan
a961ffe2e1 Bump github.com/golangci/golangci-lint 2021-09-09 00:14:18 -04:00
Deluan
402aaaaf45 Bump github.com/lestrrat-go/jwx version 2021-09-09 00:13:39 -04:00
Deluan
5c7784f842 Bump github.com/cespare/reflex version 2021-09-09 00:13:06 -04:00
Deluan
a4e96d25a8 Pin Node to 16.8 as a workaround to https://github.com/nodejs/node/issues/40030 2021-09-08 23:28:28 -04:00
Deluan
8444c28bed Upgrade react-admin to 3.18.1
This makes the sidebar fixed when users scroll vertically. This supersedes #1024, even though it also hides the hamburger menu...
2021-09-08 22:50:03 -04:00
Tucker Kern
fb11080545 Improve performance of placeholder images (#1325)
* Don't include updatedAt field when fetching album art placeholder. This will allow browers to cache the place holder

* Apply resizing to placeholder image

* Fix issues discovered by CI linter and prettier

* Updates from PR review
2021-09-06 22:34:37 -04:00
whorfin
173b30cd59 Extract version from directory name if .git dir is missing (#1327)
* Extract version from directory name if .git dir is missing

* Avoid using shell

* Remove .gitinfo build from pipeline
2021-09-06 21:26:04 -04:00
Deluan
af7c87dd7b Give a warning on commands that do not build the frontend.
This is to avoid confusions like this:
https://github.com/navidrome/navidrome/issues/1297#issuecomment-913007331
2021-09-05 21:39:19 -04:00
Artyom
8df056b797 ru.json update (#1320) 2021-09-05 10:56:54 -04:00
caiocotts
54f98497c2 Use wchar_t for TagLib filenames on Windows (#1310)
* Use wchar_t for tagLib filenames on Windows

* Make TagLib default extractor for all platforms.

* Organize imports

Co-authored-by: Deluan <deluan@navidrome.org>
2021-08-28 19:35:54 -04:00
dependabot[bot]
c55e65902b Bump github.com/onsi/gomega from 1.15.0 to 1.16.0 (#1300)
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.15.0 to 1.16.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.15.0...v1.16.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>
2021-08-23 20:50:46 -04:00
dependabot[bot]
d37231f319 Bump github.com/ReneKroon/ttlcache/v2 from 2.7.0 to 2.8.0 (#1298)
Bumps [github.com/ReneKroon/ttlcache/v2](https://github.com/ReneKroon/ttlcache) from 2.7.0 to 2.8.0.
- [Release notes](https://github.com/ReneKroon/ttlcache/releases)
- [Changelog](https://github.com/ReneKroon/ttlcache/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ReneKroon/ttlcache/compare/v2.7.0...v2.8.0)

---
updated-dependencies:
- dependency-name: github.com/ReneKroon/ttlcache/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>
2021-08-23 20:47:36 -04:00
dependabot[bot]
47fa32ec93 Bump github.com/golangci/golangci-lint from 1.41.1 to 1.42.0 (#1299)
Bumps [github.com/golangci/golangci-lint](https://github.com/golangci/golangci-lint) from 1.41.1 to 1.42.0.
- [Release notes](https://github.com/golangci/golangci-lint/releases)
- [Changelog](https://github.com/golangci/golangci-lint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/golangci/golangci-lint/compare/v1.41.1...v1.42.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-23 17:41:55 -04:00
Deluan
cf042ed83d Fix random volume changes 2021-08-22 12:18:26 -04:00
Deluan
d481864035 Some small refactorings 2021-08-22 12:16:49 -04:00
Deluan
c2927e105b Fix loadSimilar method, was causing "internal error". Fix #1293 2021-08-21 19:37:00 -04:00
Deluan
6983390ca3 Setup git hooks when running make setup 2021-08-21 13:45:34 -04:00
Deluan
143f5ba9d5 Import song duration with hundredths when using TagLib
This is how ffmpeg extractor currently works, and it makes album durations more precise.
2021-08-20 21:42:01 -04:00
Deluan
05e27095b2 Fix getTopSongs endpoint 2021-08-19 08:17:22 -04:00
Tucker Kern
aa72d3d41b Add missing song information to players and apply EnableCoverAnimation to mobile player. (#1268)
* Disable mobile player cover animation when EnableCoverAnimation is set to false. Also increase cover art size and remove rounded borders.

* Display song album and year in mobile player view

* Remove default singer element from mobile player and reduce vertical white space

* Only add song year if it exists

* Add song year to desktop player when present

* Increase non-animated cover size to 85% and set a limit on the width of 600px.

* Explain what what the styles impact

* Remove unused style for songArtist

* Apply prettier

* Adjust player styles to handle nonsquare album art better. Should probably push this upstream too

* Also fix desktop player's handling of non square cover art.
2021-08-17 13:57:48 -04:00
ToadCast
a20bd5fe05 Correct some french translations (#1278) 2021-08-13 20:27:37 -04:00
Deluan Quintão
99aeaabf7d Update translations (#1285)
* Update es.json (POEditor.com)

* Update zh-Hans.json (POEditor.com)

* Update zh-Hant.json (POEditor.com)

* Update cs.json (POEditor.com)

* Update nl.json (POEditor.com)

* Update fr.json (POEditor.com)

* Update de.json (POEditor.com)

* Update it.json (POEditor.com)

* Update ja.json (POEditor.com)

* Update pl.json (POEditor.com)

* Update pt.json (POEditor.com)

* Update sl.json (POEditor.com)

* Update es.json (POEditor.com)

* Update th.json (POEditor.com)

* Update uk.json (POEditor.com)
2021-08-13 20:27:18 -04:00
dependabot[bot]
0a5f966047 Bump redux from 4.1.0 to 4.1.1 in /ui
Bumps [redux](https://github.com/reduxjs/redux) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/reduxjs/redux/releases)
- [Changelog](https://github.com/reduxjs/redux/blob/master/CHANGELOG.md)
- [Commits](https://github.com/reduxjs/redux/compare/v4.1.0...v4.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-13 20:11:05 -04:00
dependabot[bot]
5338567346 Bump github.com/onsi/gomega from 1.14.0 to 1.15.0
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.14.0 to 1.15.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.14.0...v1.15.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>
2021-08-13 19:57:12 -04:00
dependabot[bot]
50526024b2 Bump github.com/lestrrat-go/jwx from 1.2.4 to 1.2.5
Bumps [github.com/lestrrat-go/jwx](https://github.com/lestrrat-go/jwx) from 1.2.4 to 1.2.5.
- [Release notes](https://github.com/lestrrat-go/jwx/releases)
- [Changelog](https://github.com/lestrrat-go/jwx/blob/main/Changes)
- [Commits](https://github.com/lestrrat-go/jwx/compare/v1.2.4...v1.2.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-13 19:56:57 -04:00
Deluan
db4165f27c Bum react-admin to 3.17.2. Fix SearchInput border 2021-08-13 19:45:18 -04:00
Deluan
a8cf887e87 Adjust icon size 2021-08-03 22:01:57 -04:00
Deluan Quintão
63000ab5ae Update en.json (POEditor.com) 2021-08-03 21:41:32 -04:00
Deluan
b44ef81c6f Allow 0 value to disable ScanSchedule.
Seems that Viper does not recognize empty environment vars as valid values

Closes #1274
2021-08-03 19:37:36 -04:00
caio
e9d0abe0bc Support local paths as urls in playlists. 2021-08-02 23:53:47 -04:00
Deluan
bcafe88ef9 Don't autoplay when reloading play queue from Redux store 2021-08-01 19:49:17 -04:00
dependabot[bot]
1710730b45 Bump github.com/kr/pretty from 0.2.1 to 0.3.0 (#1267)
Bumps [github.com/kr/pretty](https://github.com/kr/pretty) from 0.2.1 to 0.3.0.
- [Release notes](https://github.com/kr/pretty/releases)
- [Commits](https://github.com/kr/pretty/compare/v0.2.1...v0.3.0)

---
updated-dependencies:
- dependency-name: github.com/kr/pretty
  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>
2021-08-01 12:56:55 -04:00
Deluan
aa1571e074 Remove unused AnnotatedModel interface 2021-08-01 12:04:45 -04:00
Deluan
c831dc4cdf Use structs lib to map models to DB. Fix #1266 2021-08-01 12:04:45 -04:00
Deluan
344d7a4392 Inject DB into DataStore, instead of hardcode the dependency 2021-07-31 20:15:20 -04:00
Deluan
c0fc36da63 Make album genres clickable 2021-07-27 13:22:35 -04:00
Deluan
e68b22ea5d Don't send invalid scrobbles when clearing the player's queue 2021-07-26 16:50:50 -04:00
Sitansh Rajput
fb4eefced5 "Add to Playlist" on AlbumList actions (#1257)
* added a dependency npm was complaining about

added playlist to album actions

* removed chokidar dependency

Co-authored-by: Skrtansh Rajput <srajput@alienvault.com>
2021-07-26 15:00:38 -04:00
Deluan
615cac2ec4 Extract ExternalLinks into its own component 2021-07-26 13:25:31 -04:00
dependabot[bot]
72f9e3e80a Bump @testing-library/user-event from 13.2.0 to 13.2.1 in /ui
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 13.2.0 to 13.2.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.2.0...v13.2.1)

---
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>
2021-07-26 13:25:19 -04:00
Deluan
763bcafdac Use Tooltip in links to external sites 2021-07-24 19:52:15 -04:00
Deluan
5b81f7a73a Fix get song by id 2021-07-24 18:54:22 -04:00
Deluan
7bd506accc Retrieve all options for Genre filters 2021-07-24 16:00:27 -04:00
Deluan
b66c39451f Fix build 2021-07-24 11:45:46 -04:00
Deluan
4ed01bad86 Use ffmpeg extractor by default on Windows
This is to avoid issue with unicode chars in filenames. See #810
2021-07-24 11:35:49 -04:00
Deluan
d3975d206a Reorganize metadata extractors code 2021-07-24 11:10:19 -04:00
Deluan Quintão
6175629bb4 Build and release Docker image for Linux 32 bits platform (#1260) 2021-07-24 07:51:12 -04:00
Deluan
6c550819fd Use TagLib to detect whether a media file has embedded cover or not 2021-07-24 01:59:53 -04:00
Deluan
91325071a6 Change fallback extractor to taglib, the default option 2021-07-22 23:08:47 -04:00
Deluan
876dda83f2 Reduce number of calls to lstat.
Should make the scanner a bit faster, specially in networked filesystems
2021-07-21 22:17:37 -04:00
Deluan
86c0b422f6 Small refactorings 2021-07-21 12:46:30 -04:00
Deluan
1cef44a543 Show in the logs which mbid will be used if multiple mbids are found for album/artist 2021-07-21 11:12:03 -04:00
Deluan
4fcb238295 Fix "too many SQL variables" error in GetStarred endpoint 2021-07-21 10:45:52 -04:00
Deluan
4f9d546da4 Abort startup if config file is invalid 2021-07-20 22:35:22 -04:00
Deluan
eeb14f0243 Removed unused function 2021-07-20 20:50:59 -04:00
Deluan
a89bdfbb8d Fix build 2021-07-20 20:22:15 -04:00
Deluan
8afa2cd833 Remove dependency of deprecated ioutil package 2021-07-20 20:12:28 -04:00
Deluan
774ad65155 Use fs.FS in MergeFS implementation 2021-07-20 19:54:44 -04:00
Deluan
7540881695 Small refactorings 2021-07-20 19:18:29 -04:00
Deluan
08840f6170 Simplify cover detection in roll-up code by left-joining synthesized table 2021-07-20 17:45:08 -04:00
Deluan
cddd1b3f6b Simplify genre roll-up code by left-joining synthesized tables 2021-07-20 17:45:08 -04:00
Deluan
bc6b175414 Make getGenre Subsonic endpoint returns genres sorted by counts 2021-07-20 17:45:08 -04:00
Deluan
b6e9ec4db4 Optimize GetAll genres query 2021-07-20 17:45:08 -04:00
Deluan
1471e1240d Show songs' genres as text instead of Chips 2021-07-20 17:45:08 -04:00
Deluan
95181d748d Fix rollup of track genres to albums and artists.
See: https://github.com/navidrome/navidrome/pull/1251#issuecomment-882343022
2021-07-20 17:45:08 -04:00
Deluan
254e5673e1 Fix log message about artist with Various Artists' mbid 2021-07-20 17:45:08 -04:00
Deluan
00e418cb2a Fix log message about multiple MBIDs 2021-07-20 17:45:08 -04:00
Deluan
2742977c63 Fix multiple id3v2.4 genres appearing as one big concatenated genre 2021-07-20 17:45:08 -04:00
Deluan
69f71be98a Add more tests 2021-07-20 17:45:08 -04:00
Deluan
58ee4c60ca Add Links to external sites 2021-07-20 17:45:08 -04:00
Deluan
21cd50d81c Fix aggregated values (count, size, duration) in roll-up queries 2021-07-20 17:45:08 -04:00
Deluan
054b5eafdb Add Genres as "Chips" in Album details and Song details 2021-07-20 17:45:08 -04:00
Deluan
e2233779f1 Force full rescan when adding multi-genres 2021-07-20 17:45:08 -04:00
Deluan
3a356499ae Fix lint error 2021-07-20 17:45:08 -04:00
Deluan
a0cd585401 Fix Count methods 2021-07-20 17:45:08 -04:00
Deluan
20b7e5c49b Add Genre filters to UI 2021-07-20 17:45:08 -04:00
Deluan
c56c7c865e Purge unused genres at the end of the scan 2021-07-20 17:45:08 -04:00
Deluan
b56e034ce3 Add multiple genres to Artists 2021-07-20 17:45:08 -04:00
Deluan
1d8607ef6a Remove unnecessary repositories methods 2021-07-20 17:45:08 -04:00
Deluan
5e54925520 Add multiple genres to Albums 2021-07-20 17:45:08 -04:00
Deluan
39da741a80 Add multiple genres to MediaFile 2021-07-20 17:45:08 -04:00
Deluan
7cd3a8ba67 Add genre tables, read multiple-genres from tags 2021-07-20 17:45:08 -04:00
Deluan
1f0314021e Change initial scan message log level 2021-07-19 16:53:58 -04:00
Deluan
19c2ef3803 Enable buffered scrobbles by default 2021-07-19 16:10:37 -04:00
dependabot[bot]
d886c63122 Bump react-image-lightbox from 5.1.1 to 5.1.4 in /ui (#1252)
Bumps [react-image-lightbox](https://github.com/frontend-collective/react-image-lightbox) from 5.1.1 to 5.1.4.
- [Release notes](https://github.com/frontend-collective/react-image-lightbox/releases)
- [Changelog](https://github.com/frontend-collective/react-image-lightbox/blob/master/CHANGELOG.md)
- [Commits](https://github.com/frontend-collective/react-image-lightbox/compare/v5.1.1...v5.1.4)

---
updated-dependencies:
- dependency-name: react-image-lightbox
  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>
2021-07-19 13:20:44 -04:00
dependabot[bot]
5b828cd7ef Bump @testing-library/user-event from 13.1.9 to 13.2.0 in /ui (#1253)
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 13.1.9 to 13.2.0.
- [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.1.9...v13.2.0)

---
updated-dependencies:
- dependency-name: "@testing-library/user-event"
  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>
2021-07-19 13:20:28 -04:00
Deluan
ef60db3a5f Bump github.com/lestrrat-go/jwx from 1.2.2 to 1.2.4 2021-07-19 13:17:55 -04:00
Deluan
882b02c747 Fix forceRescan not re-importing all tracks 2021-07-19 10:32:33 -04:00
Deluan
44e7502aef Log warning when artist has a MBID of Various Artists 2021-07-18 18:28:51 -04:00
Deluan
e61cf3217d Reapply the fix from #1054, but without getting into an infinite look in case of SMB fs errors. See #1164 2021-07-17 21:06:53 -04:00
Deluan
03ad6e972a Removed unused attributes in Last.fm responses 2021-07-16 21:06:47 -04:00
Deluan
eb8ffc6f76 Fix infinite loop when the fs fails. Closes #1164 2021-07-16 09:38:58 -04:00
Deluan
b0fc684cb6 Fix small lint errors found by gocritic 2021-07-15 13:43:03 -04:00
Deluan
8d56ec898e Use AlbumArtist tag even for compilations, when it is specified.
If the tracks' AlbumArtists are different, then use "Various Artists"
2021-07-15 11:53:08 -04:00
Deluan Quintão
5064cb2a46 Add git version info to release source (#1250) 2021-07-15 09:49:34 -04:00
Deluan
f78257235e Add option to have different loglevels per source folder/file 2021-07-14 18:44:14 -04:00
dependabot[bot]
1a6a284bc1 Bump github.com/google/uuid from 1.2.0 to 1.3.0 (#1249)
Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.2.0 to 1.3.0.
- [Release notes](https://github.com/google/uuid/releases)
- [Commits](https://github.com/google/uuid/compare/v1.2.0...v1.3.0)

---
updated-dependencies:
- dependency-name: github.com/google/uuid
  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>
2021-07-14 11:09:23 -04:00
dependabot[bot]
1d948beb1f Bump github.com/go-chi/httprate from 0.5.0 to 0.5.1 (#1248)
Bumps [github.com/go-chi/httprate](https://github.com/go-chi/httprate) from 0.5.0 to 0.5.1.
- [Release notes](https://github.com/go-chi/httprate/releases)
- [Commits](https://github.com/go-chi/httprate/compare/v0.5.0...v0.5.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-14 11:07:11 -04:00
dependabot[bot]
deefd7a2fd Bump github.com/lestrrat-go/jwx from 1.2.1 to 1.2.2 (#1247)
Bumps [github.com/lestrrat-go/jwx](https://github.com/lestrrat-go/jwx) from 1.2.1 to 1.2.2.
- [Release notes](https://github.com/lestrrat-go/jwx/releases)
- [Changelog](https://github.com/lestrrat-go/jwx/blob/main/Changes)
- [Commits](https://github.com/lestrrat-go/jwx/compare/v1.2.1...v1.2.2)

---
updated-dependencies:
- dependency-name: github.com/lestrrat-go/jwx
  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>
2021-07-14 11:06:51 -04:00
Deluan
97f87416a4 Bump @testing-library dependencies 2021-07-14 11:02:11 -04:00
Deluan
5d8b90b168 Bump Go dependencies 2021-07-14 10:55:40 -04:00
Deluan
8396b51a9c Upgrade React-Admin to 3.17.0 2021-07-14 10:39:48 -04:00
Deluan
4a25fa0920 Make the default volume 50% (compensate for logarithmic volume).
Closes #1052
2021-07-14 09:58:50 -04:00
dependabot[bot]
8e71f308c2 Bump prettier from 2.3.1 to 2.3.2 in /ui (#1210)
Bumps [prettier](https://github.com/prettier/prettier) from 2.3.1 to 2.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/2.3.1...2.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>
2021-07-09 21:29:51 -04:00
Deluan
53fe2e98c5 Remove eject script, hopefully it will never be used 2021-07-08 21:09:38 -04:00
Deluan
334068c8bf Refactor mime-types configuration 2021-07-08 11:38:29 -04:00
Deluan
b34d77f85a Don't show "playing/paused" icon on the first song when calling "Play Now" 2021-07-05 21:45:20 -04:00
Deluan
24d4c81b34 Change default volume to 50%
Should fix #1052
2021-07-04 21:36:41 -04:00
Deluan
189d0c0ab3 Restore volume when playing a song...
... or continuing to play a paused one
2021-07-04 21:36:41 -04:00
Deluan
1922eaaab2 Make cover rectangular in player when cover animation is disabled 2021-07-04 12:52:59 -04:00
Deluan
5d9bea5087 Fix Album grid responsiveness on small screens.
Potentially fixes #647
2021-07-04 12:09:30 -04:00
Deluan
69afb69959 Fix Disc context menu not visible in mobile 2021-07-04 11:44:41 -04:00
Deluan
27ba267b38 Fix play single song action 2021-07-03 22:29:59 -04:00
Deluan
114fdce09e Fix Last.fm's artist.getInfo 2021-07-03 21:48:53 -04:00
Deluan
fa8b4d40b4 Fix arranging songs in PlayQueue 2021-07-03 21:34:24 -04:00
Deluan
ace5c905eb Made the Player behaviour more consistent 2021-07-02 23:36:33 -04:00
Deluan
26b5e6b1b4 Better scrobble log message when buffer is disabled 2021-07-02 10:19:16 -04:00
certuna
77f6bc83ac Update SongList.js (#1219)
Genre and Comments columns in Songs listview (hidden by default)
2021-07-02 10:18:45 -04:00
Deluan
94e36d7f60 Remove old feature flag for cache layout 2021-07-02 10:04:41 -04:00
Deluan
f49205733b Add feature flag for buffered scrobbling 2021-07-02 10:04:41 -04:00
Deluan
cfb113bd33 Disable Last.FM features based on LastFM.Enabled config option 2021-07-02 10:04:41 -04:00
Deluan
289da56f64 Implement Scrobble buffering/retrying 2021-07-02 10:04:41 -04:00
Deluan
fb183e58e9 Only encrypts NewPassword if it is not empty, when updating the user details. Fixes #1222 2021-07-01 16:09:49 -04:00
Deluan
ed286c7103 Don't rely on goroutines to send keepalive events 2021-07-01 13:31:46 -04:00
Deluan
452c8dc44b Fixed the enduring nasty "too many files open" bug!! Fix #446 2021-07-01 12:07:32 -04:00
Deluan
0c2ca2a5e4 Assign event ids in the main loop, to avoid out-of-order events 2021-07-01 10:58:41 -04:00
Deluan
5bd33455a1 Fix deadlock situation when events are sent too fast to the broker 2021-07-01 10:42:00 -04:00
Deluan
4ea0f235e1 Fix scrollbar colour for Dark/ExtraDark theme. Fixes #1216 2021-06-29 12:29:00 -04:00
Deluan Quintão
b16d473d4c Update es.json (POEditor.com) 2021-06-28 17:19:01 -04:00
Deluan
fd82b8f2dc Default for EnableCoverAnimation in dev mode is true 2021-06-28 17:18:32 -04:00
Deluan
a73f885afb Add option to disable album cover animation in the player. Closes #1185 2021-06-28 17:11:05 -04:00
Brian Schrameck
167fe46288 Addresses a bug that would prevent users from changing their own passwords, introduced as part of #1187. (#1214) 2021-06-28 16:36:14 -04:00
Deluan Quintão
cb1827ccbf Update translations (#1134)
* Update de.json (POEditor.com)

* Update ja.json (POEditor.com)

* Update cs.json (POEditor.com)

* Update nl.json (POEditor.com)

* Update fr.json (POEditor.com)

* Update de.json (POEditor.com)

* Update es.json (POEditor.com)

* Update uk.json (POEditor.com)

* Update sl.json (POEditor.com)

* Update fr.json (POEditor.com)

* Update it.json (POEditor.com)

* Update it.json (POEditor.com)

* Update cs.json (POEditor.com)

* Update sl.json (POEditor.com)

* Update de.json (POEditor.com)

* Update pt.json (POEditor.com)

* Update it.json (POEditor.com)

* Update pt.json (POEditor.com)

* Update zh-Hans.json (POEditor.com)

* Update nl.json (POEditor.com)

* Update es.json (POEditor.com)

* Update cs.json (POEditor.com)

* Update zh-Hans.json (POEditor.com)

* Update de.json (POEditor.com)

* Update fr.json (POEditor.com)

* Update sl.json (POEditor.com)

* Update ja.json (POEditor.com)

* Update uk.json (POEditor.com)

* Update cs.json (POEditor.com)

* Update nl.json (POEditor.com)
2021-06-28 09:55:46 -04:00
Deluan
25f0e11562 Add 'AlbumArtist' column to SongList 2021-06-28 09:54:17 -04:00
Deluan
292cf99f49 Add 'Year' column to Album and Playlists song list 2021-06-28 09:45:30 -04:00
Deluan
d2fcab78a5 Fix ND_DEVFASTACCESSCOVERART flag not available as env var 2021-06-26 15:40:12 -04:00
Deluan
94533e585c Add tests to /scrobble endpoint 2021-06-26 13:52:29 -04:00
Deluan
6dd38376f7 Add referential integrity to remove user's props when user is deleted 2021-06-25 23:09:10 -04:00
Deluan
26bcf0b877 Enable Last.fm scrobbling by default (still requires user's authorization) 2021-06-25 23:09:09 -04:00
Deluan
92634a7408 Only show message after 2 seconds, giving time for the browser to close it first 2021-06-25 22:23:35 -04:00
Deluan
ee21f3957e Pass userId explicitly to UserPropsRepository methods 2021-06-25 22:21:37 -04:00
Deluan
a1551074bb Add a hacky way to style the react-player. 2021-06-25 18:19:57 -04:00
Deluan
823fef8e43 Fix JS console error 2021-06-25 14:11:58 -04:00
Deluan
82105c3a16 Remove React.Strict mode 2021-06-25 14:08:00 -04:00
Deluan
b684a47f80 Show DiscSubtitle even if the album has only one disc.
Closes #947
2021-06-25 11:30:24 -04:00
Deluan
da2334e10c Remove submenu "Library". Relates to #430 2021-06-25 00:01:38 -04:00
Deluan
4853760fb5 Suppress logs of successful DB migrations applied when running for the first time 2021-06-24 23:43:20 -04:00
Deluan
0cbb0acad3 Skip songs with less than 31 seconds, as per Last.fm specification
See https://www.last.fm/api/scrobbling#when-is-a-scrobble-a-scrobble
2021-06-23 21:08:01 -04:00
Deluan
5040f6fd97 Fix label 2021-06-23 18:09:05 -04:00
Deluan
abe8015745 Add option to disable external scrobbling per player 2021-06-23 17:55:58 -04:00
Deluan
5001518260 Move user properties (like session keys) to their own table 2021-06-23 17:49:32 -04:00
certuna
265f33ed9d Remove clearServiceWorkerCache, not needed anymore. (#1205)
remove clearServiceWorkerCache, not needed anymore.
2021-06-23 12:11:35 -04:00
Deluan
99be8444d3 Disable completely external scrobblers if feature is disabled (DevEnableScrobble) 2021-06-23 11:01:58 -04:00
Deluan
f4ddd201f2 Send the time the track started playing when scrobbling 2021-06-23 11:01:58 -04:00
Deluan
056f0b944f Refactor: Consolidate scrobbling logic in play_tracker 2021-06-23 11:01:58 -04:00
Deluan
76acd7da89 Don't send scrobbles/nowPlaying updates to Last.fm if user has not authorized 2021-06-23 11:01:58 -04:00
Deluan
8af7dab23d Fix wrong warning about ignored NowPlaying 2021-06-23 11:01:58 -04:00
Deluan
a7509c9ff7 Send NowPlaying and Scrobbles to Last.fm 2021-06-23 11:01:58 -04:00
Deluan
d5461d0ae9 Refactor Agents to be singleton
Initial work for Last.fm scrobbler
2021-06-23 11:01:58 -04:00
Steve Richter
f9fa9667a3 Show user-friendly message when error occurs in Last.fm callback endpoint 2021-06-23 11:01:58 -04:00
Steve Richter
5fbfd9c81e Implement Last.fm account linking UI 2021-06-23 11:01:58 -04:00
Deluan
8b62a58b4c Remove limitation of only scrobbling tracks longer than 30 seconds 2021-06-22 09:59:00 -04:00
Deluan
743e469795 Use singleton in other places as well 2021-06-21 18:59:26 -04:00
Deluan
1f997357a9 Expose Last.fm's ApiKey to UI 2021-06-21 18:14:01 -04:00
Deluan
143cde37e5 Implement Last.FM Web authentication flow 2021-06-21 18:14:01 -04:00
Deluan
502a719e96 Implement Last.FM Desktop Auth flow endpoints 2021-06-21 18:14:01 -04:00
Steve Richter
8ee5c1f245 Initial Last.fm UI implementation 2021-06-21 18:14:01 -04:00
Deluan
0495e421fe Fix Last.fm API method signature 2021-06-21 18:14:01 -04:00
Deluan
ffa76bba6a Add flag to disable Scrobble config in the UI 2021-06-21 18:14:01 -04:00
Deluan
a4f91b74d2 Add Last.FM Authentication methods 2021-06-21 18:14:01 -04:00
Deluan
73e1a8fa06 Remove false-positive on new version detection 2021-06-21 17:46:26 -04:00
Deluan
877f01bd38 Show notification if server is updated 2021-06-21 13:48:39 -04:00
Deluan
47bcf719f2 Fix cookie warning 2021-06-20 13:27:50 -04:00
Deluan
197d430d15 Fix lint error 2021-06-20 12:07:34 -04:00
Deluan
4e1957ca71 Update Go dependencies 2021-06-20 12:06:21 -04:00
Deluan
25db2cb075 Add concurrency test for singleton 2021-06-20 11:51:32 -04:00
Deluan
80b2c2f3cf Try to register all playing music in GetNowPlaying 2021-06-20 11:25:15 -04:00
Deluan
97434c1789 Fix GetNowPlaying endpoint showing only the last play 2021-06-20 10:39:19 -04:00
Deluan
f8ee6db72a New implementation of NowPlaying 2021-06-20 10:39:16 -04:00
Deluan
0df0ac0715 Add logos to badges 2021-06-19 11:32:22 -04:00
Deluan
c09468e135 Option to allow auto-login during development. 2021-06-19 10:56:39 -04:00
Deluan
cf553ce812 Don't show "logout" when authenticated by Header 2021-06-18 19:08:25 -04:00
Deluan
31ea033880 Fix subsonic token when authenticating by Header 2021-06-18 19:00:13 -04:00
Deluan Quintão
66b74c81f1 Encrypt passwords in DB (#1187)
* Encode/Encrypt passwords in DB

* Only decrypts passwords if it is necessary

* Add tests for encryption functions
2021-06-18 18:38:38 -04:00
Deluan
d42dfafad4 Add username to request.Context 2021-06-18 18:28:51 -04:00
dependabot[bot]
84413b542e Bump @testing-library/jest-dom from 5.13.0 to 5.14.1 in /ui (#1176)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.13.0 to 5.14.1.
- [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.13.0...v5.14.1)

---
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>
2021-06-18 09:39:23 -04:00
Deluan
b590c31e4e Fix stream url, after changes to subsonic client api 2021-06-16 16:38:50 -04:00
Deluan
c4623d7bc3 Don't show "empty" dates 2021-06-16 12:28:49 -04:00
Deluan
e0fd1c6ad8 Add "Last Played" column to SongList 2021-06-16 11:57:02 -04:00
Deluan
86271f0412 Optimize refresh events for scrobble endpoint 2021-06-16 10:23:34 -04:00
Deluan
fb7229a53e Refech using getMany, reducing the number of API calls 2021-06-16 10:01:09 -04:00
Deluan
521d1ff2bf Disable realip middleware when using the reverse proxy authentication feature
Should fix https://github.com/navidrome/navidrome/pull/1152#issuecomment-862306847
2021-06-16 10:01:09 -04:00
Deluan
d3db41ae7d Bump github.com/go-chi/httprate version 2021-06-15 19:58:29 -04:00
Deluan
8bf0089abf Bump github.com/ReneKroon/ttlcache/ and github.com/microcosm-cc/bluemonday versions 2021-06-15 19:54:18 -04:00
Deluan
b65e76293a Only send events to clients who need it
- User events (star, rating, plays) only sent to same user
- Don't send to the client (browser window) that originated the event
2021-06-15 18:59:26 -04:00
Deluan
5f6f74ff2d Always use httpClient to call APIs 2021-06-15 17:29:01 -04:00
Deluan
8383527aab Only refetch changed resources when receive a "refreshResource" event 2021-06-15 16:12:13 -04:00
Deluan
8a56584aed Removed the albumSong workaround, as React-Admin's cache seems to behave better now 2021-06-15 11:31:41 -04:00
Deluan
667701be02 Less warning messages when first running it.
They are actually `info` messages
2021-06-13 19:27:01 -04:00
Deluan
59b99d2206 No need to check for first time when authenticating. One less SQL call per request 2021-06-13 19:26:25 -04:00
Deluan
d54129ecd2 Rename app package to nativeapi 2021-06-13 19:15:41 -04:00
Deluan Quintão
03efc48137 Refactor routing, changes API URLs (#1171)
* Make authentication part of the server, so it can be reused outside the Native API

This commit has broken tests after a rebase

* Serve frontend assets from `server`, and not from Native API

* Change Native API URL

* Fix auth tests

* Refactor server authentication

* Simplify authProvider, now subsonic token+salt comes from the server

* Don't send JWT token to UI when authenticated via Request Header

* Enable ReverseProxyWhitelist to be read from environment
2021-06-13 12:46:36 -04:00
Deluan
bed2f017af Fix index of songs in downloaded playlist 2021-06-12 23:02:34 -04:00
Igor Rzegocki
6bd4c0f6bf Reverse proxy authentication support (#1152)
* feat(auth): reverse proxy authentication support - #176

* address PR remarks

* Fix redaction of UI appConfig

Co-authored-by: Deluan <deluan@navidrome.org>
2021-06-11 23:17:21 -04:00
Deluan
b445cdd641 Use a dedicated api-key/secret pair for Last.FM 2021-06-10 15:07:06 -04:00
Deluan
e31802d2d3 Only send "refresh" event if SetRating was successful 2021-06-10 15:03:30 -04:00
Deluan
cefc939909 Trigger UI refresh on media annotation events: star, setRating and scrobble 2021-06-10 12:20:52 -04:00
Deluan
2afb2db7ef Refactor for readability 2021-06-09 22:35:20 -04:00
Deluan
7f85ecd515 Trigger a UI refresh when the scanner finds changes.
Closes #1025
2021-06-09 21:02:20 -04:00
dependabot[bot]
cb6aa49439 Bump github.com/lestrrat-go/jwx from 1.2.0 to 1.2.1 (#1167)
Bumps [github.com/lestrrat-go/jwx](https://github.com/lestrrat-go/jwx) from 1.2.0 to 1.2.1.
- [Release notes](https://github.com/lestrrat-go/jwx/releases)
- [Changelog](https://github.com/lestrrat-go/jwx/blob/main/Changes)
- [Commits](https://github.com/lestrrat-go/jwx/compare/v1.2.0...v1.2.1)

---
updated-dependencies:
- dependency-name: github.com/lestrrat-go/jwx
  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>
2021-06-09 15:33:04 -04:00
dependabot[bot]
b7f47c8833 Bump github.com/onsi/ginkgo from 1.16.3 to 1.16.4 (#1163)
Bumps [github.com/onsi/ginkgo](https://github.com/onsi/ginkgo) from 1.16.3 to 1.16.4.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v1.16.3...v1.16.4)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo
  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>
2021-06-09 15:18:31 -04:00
dependabot[bot]
adb09c9c69 Bump @testing-library/jest-dom from 5.12.0 to 5.13.0 in /ui (#1162)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.12.0 to 5.13.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.12.0...v5.13.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>
2021-06-09 15:18:17 -04:00
dependabot[bot]
0c9e0ff886 Bump prettier from 2.3.0 to 2.3.1 in /ui (#1161)
Bumps [prettier](https://github.com/prettier/prettier) from 2.3.0 to 2.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/2.3.0...2.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>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-09 15:17:56 -04:00
Deluan
f9eec5e4dc Refactored agents calling into its own struct 2021-06-08 17:00:02 -04:00
Deluan
6c1ba8f0d0 Add tests to core.Share 2021-06-08 16:32:08 -04:00
Deluan
110e17b004 Make MockRepo names more consistent 2021-06-08 16:30:19 -04:00
Deluan
779571a086 go mod tidy 2021-06-08 15:47:34 -04:00
Yash Jipkate
af210c8903 Add Native Sharing REST API (#1150)
* Initial draft - UNTESTED

* changes to Save() and Update()

* apply col filter and limit nanoid

* remove columns to not update
2021-06-08 15:44:30 -04:00
Deluan
e80cf80d05 Move all Spotify and LastFM code into only one folder for each 2021-06-08 11:25:46 -04:00
Ye61123
182e3ec78e Update zh-Hans.json (#1160)
Complete untranslated items
2021-06-08 10:41:01 -04:00
Steve Richter
65ccd4c99d Parse ParamBool case-insensitively (#1151) 2021-06-04 23:37:01 -04:00
Deluan
bebfe296a5 Allow updating only specific columns 2021-06-02 18:40:29 -04:00
Deluan
9da9d73c1d Don't panic when taglib returns an error 2021-05-31 18:26:46 -04:00
Deluan
cd242695ba Foundational work to enable multi-valued tags 2021-05-31 18:08:12 -04:00
Deluan
519c89345e Omit empty fields from Native API responses 2021-05-31 12:20:21 -04:00
Deluan
336d891e58 Bump github.com/ReneKroon/ttlcache/v2 from 2.5.0 to 2.6.0 2021-05-31 10:49:42 -04:00
Deluan
9b4b28f685 Bump ginkgo/gomega versions 2021-05-31 10:32:37 -04:00
Deluan
39c560a5c2 Remove unused web-vitals package 2021-05-31 10:21:24 -04:00
Deluan
c5abdc19bc Fix recursive bug in Last.FM calls without mbid 2021-05-30 22:46:23 -04:00
Deluan
ead2095dd0 Respect EnableLogRedacting config when pretty printing configuration 2021-05-30 16:02:23 -04:00
Yash Jipkate
7b05c49215 Add devEnableShare config option (#1141)
* add devEnableShare config option

* Toggle in config.js
2021-05-30 15:36:10 -04:00
Yash Jipkate
327c259a3d Create share table and repository. (#930)
* Add share table and repository

* Add datastore mock

* Try fixing indent

* Try fixing indent - 2

* Try fixing indent - 3

* Implement rest.Repository and rest.Persistance

* Renew date

* Better error handling

* Improve field name

* Fix json name conventionally
2021-05-30 11:50:35 -04:00
Deluan
675cbe11b3 Fix updatePlaylist not updating fields comment and public.
Fix #1140
2021-05-29 17:16:56 -04:00
Deluan
91a91f7e06 GetCoverArt returns placeholder if id is missing
This mimics Subsonic behaviour, even if it contradicts the API documentation, which states `id` is required

Fixes #1139
2021-05-29 11:37:00 -04:00
Deluan
7bbb09e546 Add tests for WeightedRandomChooser 2021-05-28 23:51:56 -04:00
Deluan
dd56a7798e Rename variable with conflicting name 2021-05-28 23:00:39 -04:00
Deluan
a38e478a47 Better SimilarSongs algorithm 2021-05-28 22:55:34 -04:00
Deluan
1940267a18 Handle functions with params in sort order.
Related to #1023
2021-05-28 17:35:32 -04:00
Deluan
01f3ce0228 Add a timeout to background task 2021-05-28 11:37:53 -04:00
Deluan
48b6fa7feb Don't use request's context when refreshing artist info in background 2021-05-28 09:34:15 -04:00
Deluan
25d62cd751 Set retention time for uploaded artifacts to 7 days 2021-05-27 23:39:20 -04:00
Deluan
ed01946ace Embed Last.FM error responses, making the tests faster 2021-05-27 21:04:03 -04:00
Deluan Quintão
89b12b34be Retry calls to Last.FM without MBIDs when if returns artist invalid (#1138)
* Call Last.FM's getInfo again without mbid when artist is not found

* Call Last.FM's getSimilar again without mbid when artist is not found

* Call Last.FM's getTopTracks again without mbid when artist is not found
2021-05-27 20:53:24 -04:00
Deluan
4e0177ee53 Always update artist info, even if info is fresh 2021-05-27 20:32:26 -04:00
Deluan
b398053223 Include a shared Last.FM api key, providing zero conf ArtistInfo (bio/top songs/similar artists) 2021-05-27 16:14:24 -04:00
Deluan
db11b6b8f8 Remove decoration from reflex output 2021-05-26 12:24:02 -04:00
Deluan
60d50de8c9 Refactoring to make common components usage more uniform 2021-05-26 09:35:13 -04:00
Aldrin Jenson
0941fbc0cd Fix lag on albumList toggling (#1136) 2021-05-26 08:42:39 -04:00
Deluan
4217c75c9f Upgrade to Node v16 2021-05-25 10:53:16 -04:00
Deluan
409020a502 Bump github.com/ReneKroon/ttlcache/v2 from 2.4.0 to 2.5.0 2021-05-25 10:24:40 -04:00
Deluan
b4832c36b7 Bump github.com/golangci/golangci-lint from 1.40.0 to 1.40.1 2021-05-25 10:23:08 -04:00
Deluan
1de7366ece Bump @material-ui/lab from 4.0.0-alpha.57 to 4.0.0-alpha.58 in /ui 2021-05-25 10:16:44 -04:00
Deluan
ab1bc6194a Bump @testing-library dependencies 2021-05-25 10:13:57 -04:00
dependabot[bot]
ad4db122fb Bump hosted-git-info from 2.8.8 to 2.8.9 in /ui (#1111)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-25 10:13:40 -04:00
dependabot[bot]
200b815c67 Bump url-parse from 1.4.7 to 1.5.1 in /ui (#1107)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.4.7 to 1.5.1.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.4.7...1.5.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-25 10:12:42 -04:00
Deluan Quintão
5631493cc4 Upgrade Web UI to Create-React-App 4 and React 17 (#1105)
* Upgrade to CRA 4.0.3

* Try to fix tests. No lucky

* Fix new ESLint errors

* Fix JS tests and remove unwanted dependency. (#1106)

* Fix tests

* Fix lint

* Remove React v16 workaround (fixed in v17)

* Force eslint to break on warnings

* Lint now needs to be called explicitly in the pipeline

Co-authored-by: Yash Jipkate <34203227+YashJipkate@users.noreply.github.com>
2021-05-25 09:58:06 -04:00
Deluan
d9f268266c Rename List view mode to Table 2021-05-24 12:58:15 -04:00
Deluan
882519738f Change back mounting order, for better logs 2021-05-24 12:17:55 -04:00
Deluan
86d3a219a9 Show name of router in log 2021-05-24 11:55:39 -04:00
Deluan
1d0e75151a Update Portuguese translation 2021-05-24 11:24:28 -04:00
Deluan
107a11b445 Bump React-Admin to 3.15.2 2021-05-24 11:17:06 -04:00
Aldrin Jenson
cf8ee251ee Option to toggle fields in songs, albums & artists (#923)
* Add toggleColumns

- Add logic for toggling columns
- Add MenuComponent + useSelectedFields hook

* Refactoring

* eslint-fixes

* Typo

* skip menu in albumGridView

* add omittedFields

* Add toggling for playlists and albumSong

* Refactoring

* defaultProps - fix

* Add toggling for PlaylistSongs

* remove accidental console log

* Refactoring for future compatibility

* Hide ToggleMenu in albumGridView

* Add TopBarComponent in ToggleFieldsMenu

* Add defaultOff for useSelectedFields

* Fix edge case

* eslint fix

* Refactoring

* Add propType for forwardRef

* Fix issues

* add translation for grid and table

* add translation for grid and table

* Ignore menuBtn for spotify-ish and Ligera themes

* hide bpm by default in playlistSongs

* Add memoization

* Default album view must be Grid

Co-authored-by: Deluan <deluan@navidrome.org>
2021-05-24 11:09:06 -04:00
Deluan Quintão
6a17717e30 Update translations (#1130)
* Update zh-Hant.json (POEditor.com)

* Update cs.json (POEditor.com)

* Update nl.json (POEditor.com)

* Update ja.json (POEditor.com)

* Update pl.json (POEditor.com)

* Update es.json (POEditor.com)

* Update th.json (POEditor.com)

* Update sl.json (POEditor.com)
2021-05-24 10:21:59 -04:00
Deluan
b8a274e4e8 Move Swedish translation to right folder 2021-05-24 10:09:44 -04:00
Deluan
9800823015 Bump react-jinke-music-player from 4.24.0 to 4.24.1 in /ui 2021-05-24 10:04:37 -04:00
deeeeeebs
02606f43b8 Add Swedish translation (#1126)
* Swedish translation

* Updated and renamed to sv.json

Added further lines/translations from the english.json and corrected some of the previous translations

* Update sv.json

* Update sv.json

Ok now i'm done! :P
2021-05-24 10:03:06 -04:00
Deluan
e529390034 Remove md5-hex wrapper and use blueimp-md5 directly 2021-05-20 13:42:56 -04:00
Deluan
0ec7a305a2 Reorder Makefile dev targets 2021-05-20 13:42:29 -04:00
Deluan
b6cb81c3a3 Update Portuguese translations 2021-05-16 13:31:04 -04:00
Steve Richter
e60f2bfa3d User management improvements (#1101)
* Show more descriptive success messages for User actions

* Check username uniqueness when creating/updating User

* Adjust translations

* Add tests for `validateUsernameUnique()`

Co-authored-by: Deluan <deluan@navidrome.org>
2021-05-16 13:25:38 -04:00
dependabot[bot]
666c006579 Bump lodash from 4.17.19 to 4.17.21 in /ui (#1110)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-15 14:33:29 -04:00
Deluan
6ad94548f3 Add explicit dependency for inflection 2021-05-15 11:59:56 -04:00
Deluan
fa0e6dda5b Remove C++11 warning in macOS 2021-05-14 16:03:11 -04:00
Deluan
e047008f7d Fix test 2021-05-14 15:38:28 -04:00
Deluan
3cac00ad13 Upgrade TagLib to 1.12 2021-05-14 14:31:07 -04:00
Deluan
39d68e8287 Restore pretty formatted config options in debug level 2021-05-12 14:43:09 -04:00
Deluan
751e2d6147 Make ScanInterval=0 disable the periodic scan 2021-05-12 14:08:38 -04:00
Dnouv
74300adbc8 Fix Ligera Error (#1117)
* Fix Ligera Error

* Run make setup
2021-05-12 10:21:56 -04:00
Deluan
a484adfcfb Add Slovenian translation. Thanks @jernejml 2021-05-11 22:36:22 -04:00
Deluan
25bd36dbc5 Bump react-admin to 3.15.1 2021-05-11 22:24:24 -04:00
Deluan
87298f616f Add more explicit npm dependencies 2021-05-11 22:22:32 -04:00
Deluan
4699902369 Remove dependency on lodash.get 2021-05-11 22:08:07 -04:00
Deluan
978933aa48 Add explicit npm dependencies 2021-05-11 22:07:47 -04:00
Deluan
77e736ccfd Do not use ra-core directly 2021-05-11 21:39:53 -04:00
Deluan
a77635e883 Only setup event stream when mounting the app 2021-05-11 20:27:12 -04:00
Dnouv
0c93db816c Fix PWA notification toolbar color (#1083)
* Fix PWA notification color

* Add React hook

* Convert component into a hook

Co-authored-by: Deluan <deluan@navidrome.org>
2021-05-11 20:11:54 -04:00
Deluan
c0243580c0 Integrate goose log with our own log system 2021-05-11 19:08:06 -04:00
Deluan
22ce5b6282 Removed unnecessary code 2021-05-11 18:55:58 -04:00
Deluan
fa9083ddec Upgrade prettier to 2.3.0
Some reformatting was needed... :/
2021-05-11 18:13:03 -04:00
Deluan
da684ff44c Bump github.com/lestrrat-go/jwx from 1.1.6 to 1.2.0 2021-05-11 17:44:00 -04:00
Deluan
7d96167abc Upgrade to go-chi 5 2021-05-11 17:21:18 -04:00
Deluan
fb5840705e Bump github.com/golangci/golangci-lint from 1.39.0 to 1.40.0 2021-05-11 12:30:11 -04:00
Dnouv
089d4abab1 Replace Feature Policy with Permissions Policy (#1112)
* Add Permissions Policy

* Remove Display capture option
2021-05-11 11:29:55 -04:00
Paul TREHIOU
62ccbaad8b Improve systemd unit security (#677)
Applied suggestions from `systemd-analyze` and also using StateDirectory to ensure /var/lib/navidrome exists and is writeable
2021-05-09 11:59:08 -04:00
Deluan
8419a2a5d1 Schedule periodic scan before starting initial scan 2021-05-08 19:19:31 -04:00
Aniket Biswas
71c2ed9922 Restart Current Song on previous (#1104)
* fixed on previous song behaviour

* rebased with master
2021-05-08 14:27:33 -04:00
Deluan
72ec808a2c Bump react-jinke-music-player from 4.21.2 to 4.24.0 in /ui 2021-05-08 13:15:39 -04:00
Deluan
702a65059f Fix redaction for query parameters. Fix #1103 2021-05-07 21:42:35 -04:00
Deluan
3e8d3e78c2 Fix Bookmarks Subsonic support (#1099)
JSON responses were incorrect
2021-05-07 09:47:13 -04:00
Deluan
47f4e0a4de Refactor to remove some nesting 2021-05-06 20:49:26 -04:00
Deluan
1f8949929d Fix(?) possible TypeError 2021-05-06 20:46:01 -04:00
Deluan
c92a24b3e2 Bump github.com/onsi/gomega from 1.11.0 to 1.12.0 2021-05-06 18:54:45 -04:00
dependabot[bot]
cbe0d9763b Bump github.com/robfig/cron/v3 from 3.0.0 to 3.0.1 (#1098)
Bumps [github.com/robfig/cron/v3](https://github.com/robfig/cron) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/robfig/cron/releases)
- [Commits](https://github.com/robfig/cron/compare/v3.0.0...v3.0.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-06 18:46:12 -04:00
dependabot[bot]
44dd414d25 Bump github.com/microcosm-cc/bluemonday from 1.0.8 to 1.0.9 (#1056)
Bumps [github.com/microcosm-cc/bluemonday](https://github.com/microcosm-cc/bluemonday) from 1.0.8 to 1.0.9.
- [Release notes](https://github.com/microcosm-cc/bluemonday/releases)
- [Commits](https://github.com/microcosm-cc/bluemonday/compare/v1.0.8...v1.0.9)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-06 18:45:10 -04:00
Samarjeet
d85db8ffff Fix Spotify-ish playlist title is cut off (#1094) 2021-05-06 18:33:54 -04:00
dependabot[bot]
c7378c0fa5 Bump @testing-library/user-event from 13.1.5 to 13.1.8 in /ui (#1082)
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 13.1.5 to 13.1.8.
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/master/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v13.1.5...v13.1.8)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-06 18:27:02 -04:00
plr20
18696c5517 Update Czech translation (#1095)
* Update Czech translation

* Adjust translations
2021-05-06 18:22:47 -04:00
dependabot[bot]
5a5d763c19 Bump github.com/onsi/ginkgo from 1.16.1 to 1.16.2 (#1096)
Bumps [github.com/onsi/ginkgo](https://github.com/onsi/ginkgo) from 1.16.1 to 1.16.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/v1.16.1...v1.16.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-06 18:19:40 -04:00
Deluan
f8dbc41b6d Breaking change: Add ScanSchedule, allows interval and cron based configurations.
See https://pkg.go.dev/github.com/robfig/cron#hdr-CRON_Expression_Format for expression syntax.

`ScanInterval` will still work for the time being. The only situation it does not work is when you want to disable periodic scanning by setting `ScanInterval=0`. If you want to disable it, please set `ScanSchedule=""`

Closes #1085
2021-05-06 17:56:10 -04:00
Deluan
1d6aa70033 Fix possible TypeError 2021-05-06 09:49:25 -04:00
Brian Schrameck
30bb3f7b43 BPM metadata enhancement (#1087)
* BPM metadata enhancement

Related to #1036.

Adds BPM to the stored metadata about MediaFiles.

Displays BPM in the following locations:
- Listing songs in the song list (desktop, sortable)
- Listing songs in playlists (desktop, sortable)
- Listing songs in albums (desktop)
- Expanding song details

When listing, shows a blank field if no BPM is present. When showing song details, shows a question mark.

Updates test MP3 file to have BPM tag. Updated test to ensure tag is read correctly.

Updated localization files. Most languages just use "BPM" as discovered during research on Wikipedia. However, a couple use some different nomenclature. Spanish uses PPM and Japanese uses M.M.

* Enhances support for BPM metadata extraction

- Supports reading floating point BPM (still storing it as an integer) and FFmpeg as the extractor
- Replaces existing .ogg test file with one that shouldn't fail randomly
- Adds supporting tests for both FFmpeg and TagLib

* Addresses various issues with PR #1087.

- Adds index for BPM. Removes drop column as it's not supported by SQLite (duh).
- Removes localizations for BPM as those will be done in POEditor.
- Moves BPM before Comment in Song Details and removes BPM altogether if it's empty.
- Omits empty BPM in JSON responses, eliminating need for FunctionField.
- Fixes copy/paste error in ffmpeg_test.
2021-05-05 21:35:01 -04:00
Deluan
fb33aa4496 Fix possible TypeError 2021-05-05 21:14:36 -04:00
Deluan
9e559311ad Fix Album Grid flickering 2021-05-05 16:18:08 -04:00
Deluan
a5fc5f0ff6 Revert "Better way to invoke make single"
This reverts commit 73efbd90
2021-05-04 17:05:41 -04:00
Deluan
73efbd90ab Better way to invoke make single 2021-05-04 16:47:47 -04:00
Deluan
cbc4cb483d Fix QuickFilter by favourites in Album/All view 2021-05-04 16:28:19 -04:00
Deluan
986473393f Fix missing translation error in console. Closes #1038 2021-05-04 16:01:26 -04:00
Deluan
66b31644fa Upgrade React-Admin to 3.15.0 2021-05-03 22:32:30 -04:00
Deluan
b478b0af02 FIx ffmpeg output regex too rigid 2021-05-03 21:26:44 -04:00
Deluan
c3316e201e Fix cover art detection with ffmpeg 4.4 2021-05-03 20:25:31 -04:00
Deluan
874b17b8f6 Require user to provide current password to be able to change it
Admins can change other users' password without providing the current one, but not when changing their own
2021-05-03 15:03:34 -04:00
Deluan
5808b9fb71 Fix Transcodings menu 2021-05-03 13:54:08 -04:00
Deluan
c33ebabde8 Fix warning about promise being ignored 2021-05-03 13:38:34 -04:00
Deluan
7feda4bea4 Add EnableUserEditing, to control whether a regular user can change their own details (default true) 2021-05-02 17:11:12 -04:00
Deluan
2ff1c79b64 Fix EnableLogRedacting case 2021-05-02 16:49:20 -04:00
Deluan
cfbc39fb7f Add log redacting, controlled by the new EnableLogRedacting config option (default true)
Imported redacting code from https://github.com/whuang8/redactrus (thanks William Huang)
Didn't use it as a dependency as it was too small and we want to keep dependencies at a minimum
2021-05-02 16:45:16 -04:00
Deluan
2372f1d12b Change visibility of helper function 2021-05-02 15:23:51 -04:00
Deluan
490a7fcf52 Add test to Login function 2021-05-02 15:19:21 -04:00
Deluan
ad153f5f63 Fix User delete button not showing 2021-05-02 15:03:15 -04:00
Deluan
b8138ebad6 Fix create first login 2021-05-02 14:13:17 -04:00
Deluan
e3fe8399c8 Fix DevAutoCreateAdminPassword 2021-05-01 18:40:02 -04:00
Deluan
88105d5c30 Clean-up Makefile, add help 2021-05-01 11:14:24 -04:00
Deluan
b180386d23 Simplify build targets 2021-05-01 09:16:45 -04:00
Deluan
70e7bf6b5b Clean up some make targets 2021-05-01 08:51:29 -04:00
Samarjeet
d41137ad8e [Spotify-ish] Login consistent with other themes (#1073) 2021-05-01 08:48:12 -04:00
Deluan
88f2fc35cd Fix regular users not able to edit their info before logging in again 2021-04-30 17:53:17 -04:00
Deluan
bc62efb059 More auth tests 2021-04-30 10:00:03 -04:00
Deluan
eaf40efdf4 Never send passwords to the UI 2021-04-29 20:04:01 -04:00
Deluan
71dc0dddaf Show Person icon for non admin users 2021-04-29 18:26:53 -04:00
Deluan
bcda53f115 Less waiting for cache to be ready 2021-04-29 13:58:01 -04:00
Deluan
8a07bac2a2 Fix SIGUSR1 work when ScanInterval=0 2021-04-29 13:10:10 -04:00
Deluan
a35de2bfd1 Allow regular users to change their info, including password.
Should fix #199
2021-04-28 22:35:25 -04:00
Deluan
22582392a0 Fix "Failed prop type: Invalid prop variant" in console 2021-04-28 22:07:16 -04:00
Deluan
932c108e82 Fix "SharedArrayBuffer will require cross-origin isolation"
This is a workaround for React v16 while we don't upgrade to v17

See https://github.com/facebook/create-react-app/issues/10474
2021-04-28 21:42:17 -04:00
whorfin
20d2726faa Improve scanner (#1054)
* Handle subdirectories without rx permission correctly
Allow ogg files w/o metadata, having taglib behave more like ffmpeg

* Fix test for walk_dir_tree, fix full reading of files in permission-
constrained directories, allow directories with leading ellipses

* Sorted directory traversal is preferred, and cleanup tests

* Small refactoring to clean-up `loadDir` function and to remove some "warnings" from IntelliJ

Co-authored-by: Deluan <deluan@navidrome.org>
2021-04-28 19:51:02 -04:00
Samarjeet
771c91d2dd [Spotify-ish] Indicate active page number (#1068) 2021-04-28 08:57:08 -04:00
Deluan Quintão
b8173124f4 Update ja.json (POEditor.com) 2021-04-27 18:39:45 -04:00
Deluan
d1605dcfbe Replace godirwalk with standard Go 1.16 filepath.WalkDir
Should fix https://github.com/navidrome/navidrome/issues/1048
2021-04-27 11:28:47 -04:00
Deluan
10cfaad95c Bump react-redux version to 7.2.4 2021-04-26 23:22:53 -04:00
Deluan
07f6a7cc9f Bump @testing-library dependencies 2021-04-26 23:19:30 -04:00
Deluan
6e73c23704 Keepalive must return an ID to be used with dataProvider.getOne 2021-04-26 23:19:30 -04:00
Deluan
862c6d3c73 Upgrade React-Admin to 3.14.5 2021-04-26 23:19:28 -04:00
Deluan
692663680b Uses GoLang 1.16.3
Also add a target to build snapshots for a single platform
2021-04-26 17:21:59 -04:00
Deluan
0d409e37e2 Fix aspect ratio of login icon 2021-04-26 12:35:49 -04:00
dependabot[bot]
f1bd736b20 Bump jest-environment-jsdom-sixteen from 1.0.3 to 2.0.0 in /ui
Bumps [jest-environment-jsdom-sixteen](https://github.com/SimenB/jest-environment-jsdom-sixteen) from 1.0.3 to 2.0.0.
- [Release notes](https://github.com/SimenB/jest-environment-jsdom-sixteen/releases)
- [Commits](https://github.com/SimenB/jest-environment-jsdom-sixteen/compare/v1.0.3...v2.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-26 09:11:34 -04:00
Deluan
cde6626016 Fix logo aspect ratio in Safari 2021-04-25 21:16:45 -04:00
Deluan
1c7d4c5630 Improve Logo resolution in login dialog 2021-04-25 21:16:24 -04:00
Dnouv
c75314c605 Enhanced Mobile Login Screen (#953)
* Enhanced Mobile Login Screen

* Removed duplicate line of code

* Add support for desktop

* Remove conflict

* Reset button style

* Change Login
2021-04-25 21:09:23 -04:00
Deluan
b10f491de8 Fix Song details row height 2021-04-24 23:06:14 -04:00
caiocotts
b671d0ff7b Better handling of album comments (#1013)
* Change album comment behaviour

* Don't check first item

* Fix previously imported album comments.

* Remove song comments if album comment is present
2021-04-24 21:40:55 -04:00
Deluan
4b5a5abe1b Fix Web Scroller compatibility
This fixes https://github.com/web-scrobbler/web-scrobbler/issues/2828
2021-04-24 21:13:14 -04:00
Deluan
3cede28161 Reorganize AudioTitle classes.
Should fix https://github.com/web-scrobbler/web-scrobbler/issues/2828
2021-04-24 18:06:24 -04:00
Deluan
79bbff0e98 Make Playlist grid more responsive 2021-04-24 11:29:12 -04:00
Deluan
0142352280 Fix build tag 2021-04-23 21:42:22 -04:00
Deluan
d5c7a81888 Disable SIGUSR1 handler for Windows (not available) 2021-04-23 21:22:04 -04:00
Deluan
1e539f4e54 Add trigger scan when receiving SIGUSR1 signal 2021-04-23 20:40:28 -04:00
Dnouv
e83a0b23a3 Hide volume bar in lower resolutions (#889)
This gives more space for the song and artist names in the player

* fix min-width of AlbumDetails

* Fix song play time display

* Song duration display fix#2

* Removed important

* Resolve conflicts

* Update Player.js

* Change breakdown and hide volume

Co-authored-by: Deluan <deluan@navidrome.org>
2021-04-23 19:04:37 -04:00
dependabot[bot]
9f39f062d8 Bump @testing-library/user-event from 13.1.2 to 13.1.5 in /ui (#1051)
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 13.1.2 to 13.1.5.
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/master/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v13.1.2...v13.1.5)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-23 18:44:32 -04:00
Yash Jipkate
df57cd6bb5 Allow adding songs to multiple playlists at once. (#995)
* Add support for multiple playlists

* Fix lint

* Remove console log comment

* Disable 'check' when loading

* Fix lint

* reset playlists on closeAddToPlaylist

* new playlist: accomodate string type on enter

* Fix lint

* multiple new playlists are added correctly

* use makestyle()

* Add tests

* Fix lint
2021-04-23 18:37:08 -04:00
Arbaz Ahmed
d829a63686 fix: refactored some styles in jinkie player and removed br tag - #865 (#1047)
* refactored some styles in jinkieplayer

* fix: refactored some styles in jinkie player and removed br tag - #865

* fix: refactored some styles in jinkie player and removed br tag - #865

Signed-off-by: armedev <epiratesdev@gmail.com>
2021-04-23 18:06:39 -04:00
Deluan Quintão
4b061427ad Update translations (#1002)
* Update cs.json (POEditor.com)

* Update nl.json (POEditor.com)

* Update fr.json (POEditor.com)

* Update ja.json (POEditor.com)

* Update pt.json (POEditor.com)

* Update es.json (POEditor.com)

* Update uk.json (POEditor.com)

* Update zh-Hans.json (POEditor.com)

* Update zh-Hant.json (POEditor.com)

* Update eo.json (POEditor.com)

* Update de.json (POEditor.com)

* Update zh-Hant.json (POEditor.com)

* Update cs.json (POEditor.com)

* Update cs.json (POEditor.com)
2021-04-22 23:12:02 -04:00
Deluan
aa9cf8ef17 Add a cleanup to tests 2021-04-22 14:04:48 -04:00
Deluan
240de70026 Add tests for SpreadFS 2021-04-22 14:02:42 -04:00
Shishir A S
6da9dee7d3 Fade in QualityInfo while hovering on Song title (#1041)
* feat(Player/QualityInfo) : Animate Quality Info + Increased audio player dimensions

Signed-off-by: Shishir <shishir.srik@gmail.com>

* fix(Player.js) : Converted JS hover functionality to pure CSS

Signed-off-by: Shishir <shishir.srik@gmail.com>

* Removed unused useState

* fix(Player) : Reverted player height adjustment

Signed-off-by: Shishir <shishir.srik@gmail.com>
2021-04-22 09:53:33 -04:00
Deluan
467eb345ad Don't panic if fscache could not be initialized due to a FS error 2021-04-21 23:39:23 -04:00
Deluan
31b553e972 Add missing error log message in fscache initialization 2021-04-21 14:15:42 -04:00
Deluan
da30923a95 Replace default Login backgrounds with Navidrome's collection 2021-04-20 15:26:24 -04:00
dependabot[bot]
e7be2f6f9c Bump ssri from 6.0.1 to 6.0.2 in /ui (#1045)
Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/npm/ssri/releases)
- [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md)
- [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-20 15:06:46 -04:00
Deluan
9a509c749a Workaround for https://github.com/lijinke666/react-music-player/issues/351 2021-04-20 11:01:31 -04:00
Deluan
abaecf2b88 Add Nginx header to not buffer SSE connection
This should allow the Activity Panel, that uses a Server-Side Events/ Event Source connection, to work with default Nginx reverse proxy configuration.
2021-04-20 10:16:20 -04:00
Deluan
f63a912341 Add config option to set default theme 2021-04-18 13:51:00 -04:00
Deluan
b6f525bda5 Fix exception when running in Firefox over insecure http. Fix #1039 2021-04-18 02:21:58 -04:00
Deluan
0063720cc2 Change size and position of QualityInfo in the Player 2021-04-17 22:49:36 -04:00
Ruchi Kushwaha
b441260186 Change icon on active menu item (#903)
* add icons

* add logic to change the icon

* make the active menu bold

* Encapsulate the dynamic icon behaviour into a self-contained component

Co-authored-by: Deluan <deluan@navidrome.org>
2021-04-17 00:40:07 -04:00
Deluan
16a5ac323b Fix migration error caused by #868 2021-04-15 21:25:02 -04:00
Praveen Kumar
749f5d45c6 Fix welcome message styles (#1015)
* style(login): welcome-message-wrapping - #1014

Signed-off-by: Praveen Kumar <pkspyder007@gmail.com>

* style(login): welcome-message-wrapping - #1014

Signed-off-by: Praveen Kumar <pkspyder007@gmail.com>

* chore(makefile): Removed-lint-timeout

Signed-off-by: Praveen Kumar <pkspyder007@gmail.com>
2021-04-15 20:18:35 -04:00
Deluan
a81ef0923b Fix cover art not showing in notification when using a BaseURL 2021-04-14 13:30:14 -04:00
Samarjeet
c86d2a93b1 Fix transparent background in Spotify-ish (#1030) 2021-04-13 21:05:25 -04:00
Deluan
b55d582882 go mod tidy 2021-04-13 16:55:26 -04:00
Deluan
6635149f3c Fix pre-commit hook 2021-04-13 16:54:31 -04:00
Deluan
01b34f4f14 Bumps @testing-library/user-event from 13.1.1 to 13.1.2 2021-04-13 16:52:11 -04:00
Deluan
cb9cabe0ec Bumps github.com/ReneKroon/ttlcache/v2 from 2.3.0 to 2.4.0 2021-04-13 16:48:41 -04:00
Deluan
29aff05f70 Bump github.com/onsi/ginkgo from 1.16.0 to 1.16.1 2021-04-13 16:48:05 -04:00
Deluan
f48bfb6ad1 Bump github.com/microcosm-cc/bluemonday version 2021-04-13 16:47:31 -04:00
Deluan
ef9a16ac9f Change order of themes 2021-04-10 19:52:01 -04:00
Dnouv
ca9d42714f New Ligera (light) Theme (#990)
* Enhanced Light Theme

* New Login Screen

* Fix Appbar for sm screen

* Reverse Gradient

* Fix test error

* Fix color

* Fix Gradient

* Theme color change

* Fix playlist autocomp popup

* Rename theme

* Fix hover icon color
2021-04-10 19:49:39 -04:00
Deluan
efe8240c0a Remove inline style in favour of MUI's styling solution 2021-04-09 17:38:08 -04:00
Ayush Naidu
f7dfabaa40 Replaced literal 302 with http constant (#1006) 2021-04-08 14:17:14 -04:00
Deluan
d8a1773d50 Increase golangci-lint timeout. Fix #1001 2021-04-08 10:19:58 -04:00
Aries
e105e2d2a2 Update Japanese translation (#997) 2021-04-07 23:18:49 -04:00
Deluan
f41bc31ba8 Fix layout when album comment is visible 2021-04-07 22:16:38 -04:00
Deluan
96a14ec484 Hide QualityInfo on small screens 2021-04-07 16:10:31 -04:00
Neil Chauhan
48ae4f7479 Add 5-star rating system(#986)
* Added Star Rating functionality for Songs

* Added Star Rating functionality for Artists

* Added Star Rating functionality for AlbumListView

* Added Star Rating functionality for AlbumDetails and improved typography for title

* Added functionality to turn on/off Star Rating functionality for Songs

* Added functionality to turn on/off Star Rating functionality for Artists

* Added functionality to turn on/off Star Rating functionality for Albums

* Added enableStarRating to server config

* Resolved the bugs and improved the ratings functionality.

* synced repo and removed duplicate key

* changed the default rating size to small, and changed the color to match the theme.

* Added translations for ratings, and Top Rated tab in side menu.

* Changed rating translation to topRated in albumLists, and added has_rating filter to topRated.

* Added empty stars icon to RatingField.

* Added sortable=false in AlbumSongs and added sortByOrder=DESC in all List components.

* Added translations for rating, for artists and albums, and removed the sortByOrder=DESC from SimpleLists.
2021-04-07 16:02:52 -04:00
Deluan
840521ffe2 Fix console errors for QualityInfo component 2021-04-07 13:08:41 -04:00
Deluan
5178f44094 Add has_rating filter to albums 2021-04-07 11:04:36 -04:00
Deluan
10dcc3fb37 Remove unnecessary export mapping (bad refactoring) 2021-04-07 10:57:14 -04:00
Aries
49b1c40fbd Update Japanese translation (#992) 2021-04-07 10:14:45 -04:00
Deluan
156a53c2ac Add support for artist 5-star rating in Subsonic API 2021-04-06 23:06:31 -04:00
Deluan
9913b92905 Get lossless format list from server 2021-04-06 22:18:48 -04:00
Himanshu maurya
52812fa48b Added quality info (#918)
* added quality info

* fixed formatting

* implemented various suggestions

* npm run prettier

* applied suggestions

* npm run prettier

* corrected lossless formats and other suggestions

* moved losslessformats into consts.js

* added some test

* typo while resolving conflicts

* fetch

* removed a bug causing component (as suggested)

* Update PlayerToolbar.js

* implemented suggestions

* added few more tests

* npm run prettier

* added size

* updated qualityInfo

* implemented suggestions

* added test for when no record is recieved

* Update QualityInfo.js
2021-04-06 21:30:17 -04:00
Shishir A S
c57fa7a8fc Fixes play icon color in "Light" theme (#972)
* fix(ui/src/album): White play button on hover for all themes - #960

* fix(PlayButton) : White play button for light theme - #960

* fix(PlayButton) : White play button for light theme - #960

* bug(AlbumGridView.js) - Album play button defaults to white, can be overridden - #960

Signed-off-by: Shishir <shishir.srik@gmail.com>

* bug(AlbumGridView.js) - Album play button defaults to white, can be overridden - #960

* Reverted package.json and package-lock.json - #960

Signed-off-by: Shishir <shishir.srik@gmail.com>

* Missing lint script added - #960

Signed-off-by: Shishir <shishir.srik@gmail.com>

* Removed color, added className and made record required in PlayButton.propTypes - #960
2021-04-06 10:02:44 -04:00
Deluan
dbda8712f2 npm audit fix 2021-04-05 22:33:13 -04:00
Deluan
5f685bca30 Hide ❤️ in Playlists 2021-04-05 22:32:25 -04:00
Deluan
37f6ff02cf Do not disable eslint rule 2021-04-05 22:21:05 -04:00
dependabot[bot]
01ef4d2f21 Bump @testing-library/jest-dom from 5.11.9 to 5.11.10 in /ui
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.11.9 to 5.11.10.
- [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.11.9...v5.11.10)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-05 21:21:14 -04:00
dependabot[bot]
32ad982b11 Bump github.com/golangci/golangci-lint from 1.38.0 to 1.39.0
Bumps [github.com/golangci/golangci-lint](https://github.com/golangci/golangci-lint) from 1.38.0 to 1.39.0.
- [Release notes](https://github.com/golangci/golangci-lint/releases)
- [Changelog](https://github.com/golangci/golangci-lint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/golangci/golangci-lint/compare/v1.38.0...v1.39.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-05 21:20:57 -04:00
dependabot[bot]
87b04607ad Bump @testing-library/react-hooks from 5.1.0 to 5.1.1 in /ui
Bumps [@testing-library/react-hooks](https://github.com/testing-library/react-hooks-testing-library) from 5.1.0 to 5.1.1.
- [Release notes](https://github.com/testing-library/react-hooks-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-hooks-testing-library/blob/master/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-hooks-testing-library/compare/v5.1.0...v5.1.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-05 21:17:18 -04:00
dependabot[bot]
4e41ef7542 Bump github.com/microcosm-cc/bluemonday from 1.0.4 to 1.0.6
Bumps [github.com/microcosm-cc/bluemonday](https://github.com/microcosm-cc/bluemonday) from 1.0.4 to 1.0.6.
- [Release notes](https://github.com/microcosm-cc/bluemonday/releases)
- [Commits](https://github.com/microcosm-cc/bluemonday/compare/v1.0.4...v1.0.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-05 21:14:41 -04:00
dependabot[bot]
6af45d6eea Bump @testing-library/user-event from 13.0.7 to 13.1.1 in /ui
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 13.0.7 to 13.1.1.
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/master/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v13.0.7...v13.1.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-05 21:14:13 -04:00
dependabot[bot]
69fe771819 Bump @testing-library/react from 11.2.5 to 11.2.6 in /ui
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 11.2.5 to 11.2.6.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/master/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v11.2.5...v11.2.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-05 21:10:30 -04:00
dependabot[bot]
ce675d478a Bump github.com/onsi/ginkgo from 1.15.2 to 1.16.0
Bumps [github.com/onsi/ginkgo](https://github.com/onsi/ginkgo) from 1.15.2 to 1.16.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/v1.15.2...v1.16.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-05 21:10:18 -04:00
Balaguru Ragupathi
6988b9a086 Improved Header Readability for Songs List (#985)
* style(SongDataGrid): Table Header Definition - #943

Signed-off-by: Balaguru4580 <balaguru4580@gmail.com>

* style(SongDataGrid): Improved Header Readability - #943

Signed-off-by: Balaguru4580 <balaguru4580@gmail.com>

* Shadow Effect

* Shadow Effect Opacity Adjustment

* Fixed Songs Context Menu

* Fixed the Songs Context Menu
2021-04-05 20:32:57 -04:00
Aldrin Jenson
55c2431b17 Fix undefined variant prop in DateField (#987) 2021-04-05 20:27:40 -04:00
Ritik Pandey
69ee17402f Add pagination to playlists (#969)
* add pagination

* prettier applied

* perPage_bug_fixed

* pagination_component_changed

* getAllSongs function added

* pagination component updated

* catch_error from data provider

* getAllSongsAndDispatch added

* remove ids from action function
2021-04-05 18:21:47 -04:00
Deluan
cdfdf78c73 Revert "style(SongDataGrid): Improved Header Readability (#954)"
This reverts commit 3d58c5ab. It broke the SongContextMenu
2021-04-04 21:46:44 -04:00
sobhanbera
ca51372d8d Add Extra Dark theme (#955)
* added new theme - night

* removed a unused field

* fixed a typo from previous change

* night theme in login window

* changed name

changed the theme name from "Night" to "Extra Dark"

* changed the theme name

* Update index.js

* Rename night.js to extradark.js

* trying something

* formatted

the JS build was failing because I haven't formatted the index.js file with prettier. I got to know about this now.
I think now it will be resolved.
2021-04-04 21:25:54 -04:00
Éric Gaspar
a4d07734cd Update fr.json (#975) 2021-04-04 20:53:47 -04:00
Balaguru Ragupathi
3d58c5ab54 style(SongDataGrid): Improved Header Readability (#954)
* style(SongDataGrid): Table Header Definition - #943

Signed-off-by: Balaguru4580 <balaguru4580@gmail.com>

* style(SongDataGrid): Improved Header Readability - #943

Signed-off-by: Balaguru4580 <balaguru4580@gmail.com>

* Shadow Effect

* Shadow Effect Opacity Adjustment
2021-04-02 22:15:59 -04:00
Aldrin Jenson
12223b2a83 Fix extra multiline Prop error (#966)
* Fix extra multiline Prop error

* Remove multiline prop from MultiLineTextField
2021-04-02 22:09:28 -04:00
Samarjeet
c7dc3628e2 Fix transparent bg in suggestions [Spotify-ish] (#964) 2021-04-02 13:30:27 -04:00
certuna
9871919fae New service worker (#952)
* Add files via upload

* Update serviceWorker.js
2021-04-02 11:58:45 -04:00
Deluan
0cb7d3853f Add required prop order in random album list. Fix #957 2021-04-01 23:14:59 -04:00
rochakjain361
d0d18e8512 Album details UI fix (#922)
* Fix the Album Details UI to look similar to Song Details UI

* Remove the unused components

* Fix the gap between row and the first field in the details view

* Fix the width of the row of Album Details UI
2021-04-01 23:03:05 -04:00
Deluan
0d95c4bb9d Fix Code of Conduct link 2021-04-01 17:38:55 -04:00
Samarjeet
ea65da484b Make spotify-ish more spotify-ish (#914)
* [Theme] Allow customising album and player parts

* [Theme] Allow customizing song lists view

* Make spotify-ish more spotify-ish

* Fix responsive issues in spotify-ish

* Spotify-ish login page

* Add back the previous "Spotify-ish" theme as "Green"

Co-authored-by: Deluan <deluan@navidrome.org>
2021-03-31 16:58:47 -04:00
harshavardhanpb
5128c049d7 Rename diodo_test.go to diode_test.go (#956)
Simple typo fix
2021-03-31 15:41:38 -04:00
Deluan
16f6d9466f Remove redundant backgroundColor from Login icon 2021-03-31 13:31:03 -04:00
Samarjeet
cf72bbfad4 Fix login page UI contrast in dark,spotify (#946) 2021-03-31 01:11:05 -04:00
Aldrin Jenson
20f5778694 Fix prop undefined bug #925 (#942)
* fix(albumListView)  prop undefined bug  #925

* Fix undefinedProp bug
2021-03-31 01:07:07 -04:00
Deluan
46d4c48d44 Login backgrounds from unsplash collection (#936) 2021-03-31 00:49:12 -04:00
Samarjeet
166410eb50 Login backgrounds from unsplash collection (#936) 2021-03-31 00:24:37 -04:00
Deluan
43cbde97ad Remove "minimize" button from Player when in Desktop resolution 2021-03-30 22:43:39 -04:00
Deluan
13e80e651c Fix issue with classes being removed from DOM. Fix #864 2021-03-30 22:42:46 -04:00
Deluan
16e495a80f Revert: Fix theme not being applied to PlayerToolbar
It was causing issues with classes being removed from DOM
2021-03-30 22:21:24 -04:00
Samarjeet
1f2b5294c3 Allow theme customizing Login Page (#940) 2021-03-29 23:25:32 -04:00
Aldrin Jenson
a36a8c2372 Fix LinkWrapping Error in the console #921 (#924) 2021-03-29 21:45:48 -04:00
Aldrin Jenson
5245b4c62b Fix cacheUndefined bug - #901 (#915)
- add check to see if cache is defined before deleting
2021-03-29 20:43:51 -04:00
Deluan
3b0defefec Fix UI loading redirections. Should fix #906 2021-03-29 20:13:04 -04:00
Neil Chauhan
404253a881 Enable turn on/off Favorites/Loved feature. (#917)
* Added option to enable/disable favorites in Song

* Added option to enable/disable favorites in Artist

* Added option to enable/disable favorites in Albums and Favorites tab in sidebar

* Added option to enable/disable favorites in Player

* Set default value of enableFavourites as true

* Improved the functionality to enable/disable Favorites

* Add `EnableFavourites` config option

Co-authored-by: Deluan <deluan@navidrome.org>
2021-03-28 20:35:49 -04:00
Adrian Edwards
5dfcb316cf Remove unused prop from ArtistList (#926)
* remove syncWithLocation prop from ArtistList to fix #925

* run prettier
2021-03-28 19:53:25 -04:00
Deluan
90cf118349 Fix context menu/heart column header alignment in SongList 2021-03-27 22:09:43 -04:00
Deluan
b42532dd7c Fix theme not being applied to PlayerToolbar 2021-03-27 14:24:59 -04:00
Neil Chauhan
ac37bf3631 Refactored the current ️/Star feature to ❤️/Love/Favourite feature. (#908)
* Added setRating feature to AlbumListView

* Refactored the iconography from  to ❤️

* Refactored the current component from StarButton to LoveButton

* Refactored all translations from Starred to Loved, and all props from showStar to showLove

* Refactored useToggleStar hook to useToggleLove

* rebased repository from master and removed stray commmits

* Refactored handler name from TOGGLE_STAR to TOGGLE_LOVE in PlayerToolbar.js

* Change "starred" translation to "Favorite"

Co-authored-by: Deluan <deluan@navidrome.org>
2021-03-26 23:56:19 -04:00
Deluan
db208600e4 Fix theme not being applied to Player's audioTitle 2021-03-26 22:22:36 -04:00
Arbaz Ahmed
01ba00ccdd New component for mobile Artist List (#891)
Fixes #890
2021-03-25 22:45:21 -04:00
Yash Jipkate
e575825c33 Add / to _ mapping for paths based on tags. (#888)
Closes  #592
2021-03-25 21:48:28 -04:00
Ritik Pandey
5abc215270 Hide BulkActionsToolbar after removing songs from playlist (#898) 2021-03-25 21:40:31 -04:00
Deluan Quintão
210f34bbbe Update CONTRIBUTING.md 2021-03-24 23:36:48 -04:00
Kushal Kumar
5e70e0702c docs(contributing.md): Contributing guidelines added (#831)
* docs(contributing.md): Contributing guidelines added - I827

Signed-off-by: k-kumar-01 <kushalkumargupta4@gmail.com>

* docs(contributing.md): Contributing guidelines added - I827

Updated the issue number to match the existing issue
Changed channel from IRC to Discord

Signed-off-by: k-kumar-01 <kushalkumargupta4@gmail.com>

* Fix typos, remove issue template

Co-authored-by: Deluan <deluan@navidrome.org>
2021-03-24 23:33:48 -04:00
Danshil Kokil Mungur
a85b70e9db feat(github): add issue templates (#892) 2021-03-24 23:21:48 -04:00
Josep Mª Domingo
515aa7108b Move logger middleware to capture routing errors (ex: 405). (#877)
* Fix #836

* Remove requestLogger middleware from MountRouter
2021-03-24 23:17:36 -04:00
rohitgeddam
cdb387b22f Properly break long comment lines. Fix #855 (#856)
Make ui responsive on smaller screen when the comment block is longer
2021-03-24 23:14:10 -04:00
Deluan
d5434d4169 Add 'lint to pre-push git hook 2021-03-24 12:21:20 -04:00
Deluan
c46aa72ede Add lint script to UI project 2021-03-24 12:05:58 -04:00
Deluan
4b68260b83 Move constant to consts.go file 2021-03-24 10:21:31 -04:00
Deluan Quintão
ba922bbfce Update translations (#894)
* Add Ukrainian translation

* Update cs.json (POEditor.com)

* Update nl.json (POEditor.com)

* Update eo.json (POEditor.com)

* Update de.json (POEditor.com)
2021-03-23 22:28:22 -04:00
dependabot[bot]
b4a2683e65 Bump @testing-library/user-event from 12.6.2 to 13.0.7 in /ui
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 12.6.2 to 13.0.7.
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/master/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v12.6.2...v13.0.7)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-23 22:26:09 -04:00
ImgBotApp
7e225c0f47 [ImgBot] Optimize images
*Total -- 8,303.62kb -> 6,130.18kb (26.17%)

/ui/src/icons/paused-light.png -- 0.81kb -> 0.29kb (63.6%)
/.github/screenshots/ss-mobile-login.png -- 1,189.03kb -> 736.30kb (38.08%)
/ui/src/icons/playing-dark.gif -- 2.29kb -> 1.58kb (31.29%)
/ui/src/icons/playing-light.gif -- 2.29kb -> 1.58kb (31.29%)
/.github/screenshots/ss-mobile-player.png -- 1,257.85kb -> 886.30kb (29.54%)
/ui/public/apple-touch-icon-152x152.png -- 9.51kb -> 6.74kb (29.21%)
/ui/public/mstile-144x144.png -- 9.63kb -> 6.88kb (28.55%)
/ui/public/mstile-70x70.png -- 6.59kb -> 4.73kb (28.22%)
/ui/public/apple-touch-icon-180x180.png -- 11.34kb -> 8.18kb (27.89%)
/ui/public/apple-touch-icon.png -- 11.34kb -> 8.18kb (27.89%)
/ui/public/apple-touch-icon-76x76.png -- 4.52kb -> 3.27kb (27.68%)
/ui/public/apple-touch-icon-120x120.png -- 7.27kb -> 5.30kb (27.2%)
/ui/public/android-chrome-192x192.png -- 13.14kb -> 9.78kb (25.59%)
/ui/public/mstile-150x150.png -- 9.57kb -> 7.20kb (24.8%)
/ui/public/apple-touch-icon-60x60.png -- 3.40kb -> 2.57kb (24.51%)
/ui/src/icons/android-icon-72x72.png -- 6.25kb -> 4.75kb (24%)
/.github/screenshots/ss-desktop-player.png -- 5,016.75kb -> 3,833.46kb (23.59%)
/resources/logo-192x192.png -- 17.10kb -> 13.21kb (22.78%)
/ui/public/android-chrome-512x512.png -- 38.32kb -> 29.78kb (22.3%)
/ui/public/mstile-310x310.png -- 21.04kb -> 16.49kb (21.61%)
/ui/public/mstile-310x150.png -- 10.35kb -> 8.12kb (21.56%)
/resources/navidrome-600x600.png -- 369.74kb -> 297.43kb (19.56%)
/ui/public/favicon-32x32.png -- 2.18kb -> 1.80kb (17.52%)
/.github/screenshots/ss-mobile-album-view.png -- 282.95kb -> 236.01kb (16.59%)
/ui/src/icons/paused-dark.png -- 0.34kb -> 0.29kb (14.25%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2021-03-23 10:29:13 -04:00
Arbaz Ahmed
4e44d841dd New component for song display in song list (#833)
* added new component SONGSIMPLELIST for smaller displays

* added new component SONGSIMPLELIST for smaller displays

* added new component SONGSIMPLELIST for smaller displays

* Updated songsimplelist

Removed truncation

* removed garbage code

* refactored some issues of overlapping

* refactored some issues of overlapping

* changed the song ui design

* refactored some bugs in artist display

* refactored some bugs in artist display

* removed garbage dependencies

* removed div bugs

* added all the logic to the component itself
2021-03-23 00:12:19 -04:00
rochakjain361
b552eb15b3 Make the version number clickable for the SNAPSHOT version in development docker build (#843)
* Make the version number clickable for the SNAPSHOT version while using development docker build

* Update the snapshot version link in to view list of commits since the release

* Create a new component for the link to the version

* Add tests and refactored a bit

Co-authored-by: Deluan <deluan@navidrome.org>
2021-03-22 23:34:34 -04:00
Deluan
190bcd836e Bump @testing-library's dependencies 2021-03-22 15:40:06 -04:00
dependabot-preview[bot]
488a05bc5a Create Dependabot config file 2021-03-22 15:12:29 -04:00
Deluan
2daefb851a Bump golangci-lint to 1.38.0 2021-03-22 14:52:16 -04:00
Deluan
7414216094 Bump gomega and ginkgo versions 2021-03-22 14:49:57 -04:00
Deluan
300a0292ba Add timestamp indexes 2021-03-22 13:38:20 -04:00
Deluan
a4abe6ea2b Rename migration package to migrations 2021-03-22 13:38:04 -04:00
Nelyah
2671933791 Update Spanish translation
This also fixes some minor typos
2021-03-22 10:23:26 -04:00
Nelyah
da6556bb78 Update French translation 2021-03-22 10:23:26 -04:00
Deluan
c33c71ae6d Comment out flaky tests 2021-03-22 09:52:29 -04:00
certuna
1ea3d005e8 clear the ServiceWorker cache on logout (#854)
* Update authProvider.js

This fixes https://github.com/navidrome/navidrome/issues/613 : clears the ServiceWorker cache on logout. Based on this code: https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage/keys . Seems to work on macOS and iOS, but please test on other platforms.

* Format code with `prettier`

Co-authored-by: Deluan <deluan@navidrome.org>
2021-03-21 14:34:10 -04:00
Ritik Pandey
9fb55d4025 Add duplicate song warning. Fix #554
* duplicate_song_warning added

* dialog_for_multiple_songs

* skip button updated

* duplicate_song_skip import removed

* duplicate_song msg updated

* handleSkip and checkDuplicateSong func modified

* Update AddToPlaylistDialog.js

* prettier applied

* go.sum file added

* duplicated songs bug fixed
2021-03-21 13:29:35 -04:00
Yash Jipkate
3e0e11c01e Fix #260: Add Auto theme preference to set theme automatically. (#835)
* Auto theme preference added

* Fix lint

* Add and use AUTO from consts

* Add shared custom hook to get current theme

* Moved up 'Auto' choice

* AUTO -> AUTO_THEME_ID & extract useCurrentTheme to file

* Liberalise theme setting

* Add tests
2021-03-21 13:19:43 -04:00
Deluan
fa479f0a9a Show go mod download commands 2021-03-17 10:33:40 -04:00
Ketan Gupta
8b6e32588c improved makefile message to developer 2021-03-17 10:27:55 -04:00
Dnouv
b62c270b7f fix min-width of AlbumDetails 2021-03-16 23:19:11 -04:00
Ye61123
5488a829c7 Update zh-Hans.json
Improve the simplified Chinese translation accuracy and fix the parts that are not translated.
2021-03-16 23:13:40 -04:00
Daniel Morante
9d87eefd6a Create rc.d startup script for FreeBSD
Same startup script used by the port multimedia/navidrome
2021-03-16 23:11:42 -04:00
Deluan
03afb4ff0e Call go mod tidy after go mod download to undo any changes to go.sum 2021-03-16 12:49:20 -04:00
Deluan
63b9353452 Lowering the expectations caused by the name :) 2021-03-14 12:51:08 -04:00
k-kumar-01
72d6df15c6 style: New Theme Added - Spotify
Signed-off-by: k-kumar-01 <kushalkumargupta4@gmail.com>
2021-03-14 12:51:08 -04:00
Deluan
18fda0d954 Update DevContainer to GoLang 1.16 2021-03-12 18:43:03 -05:00
Deluan
34516ccf97 Bump github.com/sirupsen/logrus from 1.7.0 to 1.8.1 2021-03-12 18:19:39 -05:00
Deluan
720e2357b7 Add option to sort Recently Added by file's mtime instead of time of import. 2021-03-12 18:18:35 -05:00
Deluan
1ec105a245 Invalidate cached images when album changes 2021-03-12 15:41:11 -05:00
Deluan
0049d8d311 Bump GoLang to 1.16.2 for releases 2021-03-12 15:14:12 -05:00
Deluan Quintão
2d528bbc87 Remove dependency of go-bindata (#818)
* Use new embed functionality for serving UI assets

* Use new embed functionality for serving resources. Remove dependency on go-bindata

* Remove Go 1.15
2021-03-12 11:06:51 -05:00
rochakjain361
5a259ef3ff Make the version number in the about dialog clickable (#817)
* Make the version number in the about dialog clickable

* Fix prettier errors

* Fix build errors
2021-03-12 10:26:41 -05:00
Nelyah
43f2d82956 Accept more recent node and Go versions when building dev or server
This will allow developers to experiment with different versions of Go.
2021-03-11 23:02:13 -05:00
Nelyah
dff18101bb Bump Go version number in go.mod to 1.16
This will allow to use 'embed' and 'fs' packages.
This also makes the check for the Go environment when running the
Makefile fail if Golang version isn't 1.16.x
2021-03-11 23:02:13 -05:00
dpirad007
6cf15748c4 SelectPlaylist Input mobile view fix 2021-03-11 22:53:55 -05:00
Deluan
54a3394559 Upgrade to GoLang 1.16.0 2021-02-19 21:00:38 -05:00
Deluan
8436e18175 Update pipeline tests to Go 1.16.0 2021-02-19 19:52:14 -05:00
Deluan
a140c222c2 Fix race condition in test 2021-02-19 19:36:55 -05:00
Deluan
64ceb5371b Revert "Bump github.com/go-chi/chi from 1.5.1 to 1.5.2"
This caused a panic and needs more investigation:

http: superfluous response.WriteHeader call from github.com/go-chi/chi/middleware.Recoverer.func1.1 (recoverer.go:33)

 panic: interface conversion: *middleware.compressResponseWriter is not io.ReaderFrom: missing method ReadFrom

 -> github.com/go-chi/chi/middleware.(*httpFancyWriter).ReadFrom
 ->   /Users/deluan/go/pkg/mod/github.com/go-chi/chi@v1.5.2/middleware/wrap_writer.go:135
2021-02-15 14:21:43 -05:00
Deluan
7eb99d0b8d Remove duplicated call to split 2021-02-15 14:18:57 -05:00
Deluan
07f815edfd go mod tidy 2021-02-15 14:08:49 -05:00
Deluan
781155ff39 Replace cursor with pointer when hovering over an expandable comment.
Fixes https://github.com/navidrome/navidrome/issues/637#issuecomment-778599670
2021-02-15 14:05:34 -05:00
dependabot-preview[bot]
0d6717ce69 Bump github.com/spf13/cobra from 1.1.1 to 1.1.3
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.1.1 to 1.1.3.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Changelog](https://github.com/spf13/cobra/blob/master/CHANGELOG.md)
- [Commits](https://github.com/spf13/cobra/compare/v1.1.1...v1.1.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-02-15 13:21:16 -05:00
dependabot-preview[bot]
e0198f741f Bump github.com/go-chi/chi from 1.5.1 to 1.5.2
Bumps [github.com/go-chi/chi](https://github.com/go-chi/chi) from 1.5.1 to 1.5.2.
- [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/v1.5.1...v1.5.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-02-15 13:19:53 -05:00
Deluan
cfc9162729 Use a Waiter diode, to avoid constant CPU usage. Fixes #777 2021-02-13 12:08:32 -05:00
Deluan
48847ae479 Don't break if it tries to render ContextMenu without data. Fix #776 2021-02-13 12:04:02 -05:00
CgX
fbe6ecea95 Update fr.json 2021-02-11 09:13:24 -05:00
Nelyah
9d098e2302 Update French translation 2021-02-10 10:00:33 -05:00
Deluan
107803e037 Update list of Not Implemented / Gone Subsonic API endpoints 2021-02-09 21:25:14 -05:00
Gosz
1c285439ca Update spanish translation 2021-02-09 16:47:01 -05:00
Deluan
bde8692add Update Dutch translation 2021-02-09 16:46:29 -05:00
Deluan
1d681d92d3 Better explanation of NewSpreadFS 2021-02-09 15:33:34 -05:00
Deluan
157faad028 Rename ExternalInfo to ExternalMetadata 2021-02-09 15:33:33 -05:00
Deluan
5fdd8b32d7 Move utilitarian/generic packages to utils: lastfm, spotify, gravatar, cache, and pool 2021-02-09 15:33:33 -05:00
Deluan
b855fe865e Add artist ID to agent's interfaces 2021-02-09 11:19:32 -05:00
Andy Klimczak
501af14186 Upgrade pipeline to use docker/setup-buildx-action
Issue #563
2021-02-08 21:05:27 -05:00
Deluan
29465d92a7 Add Chinese Traditional translation (thanks @Leitftw) 2021-02-08 19:09:42 -05:00
Deluan
7cc026ac35 Add some info about how to create new agents 2021-02-08 17:18:43 -05:00
Deluan
fefbe0b117 Cleanup, add Placeholder agent 2021-02-08 16:54:51 -05:00
Deluan
e5cbfac483 Implement TopSongs 2021-02-08 16:54:51 -05:00
Deluan
e1cb52689e Implement SimilarSongs 2021-02-08 16:54:51 -05:00
Deluan
84a50d5dce Use MBID in calls to Last.FM, if it is available 2021-02-08 16:54:51 -05:00
Deluan
6c1fc5f836 Clean names before calling agents 2021-02-08 16:54:51 -05:00
Deluan
a76a52e99a Get MBID first, if it is not yet available 2021-02-08 16:54:51 -05:00
Deluan
52a407b84b Clean up, comments and logs 2021-02-08 16:54:51 -05:00
Deluan
365dff6435 Fix lint errors 2021-02-08 16:54:51 -05:00
Deluan
877cdf1d5c Get images 2021-02-08 16:54:51 -05:00
Deluan
28cdf1e693 Add a cached http client 2021-02-08 16:54:51 -05:00
Deluan
9d24106066 Incomplete implementation of agents 2021-02-08 16:54:51 -05:00
dependabot-preview[bot]
d8a4db36ef Bump react-icons from 4.1.0 to 4.2.0 in /ui
Bumps [react-icons](https://github.com/react-icons/react-icons) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/react-icons/react-icons/releases)
- [Commits](https://github.com/react-icons/react-icons/compare/v4.1.0...v4.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-02-08 11:29:27 -05:00
Deluan
8799358a04 go mod tidy 2021-02-07 14:26:32 -05:00
dependabot-preview[bot]
58fd5326f5 Bump github.com/onsi/gomega from 1.10.4 to 1.10.5
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.10.4 to 1.10.5.
- [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.10.4...v1.10.5)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-02-06 22:21:02 -05:00
dependabot-preview[bot]
e9066690fd Bump github.com/onsi/ginkgo from 1.14.2 to 1.15.0
Bumps [github.com/onsi/ginkgo](https://github.com/onsi/ginkgo) from 1.14.2 to 1.15.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/v1.14.2...v1.15.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-02-06 22:09:41 -05:00
Deluan
a427d6c940 Fix Docker pulls badge 2021-02-06 22:04:01 -05:00
Deluan
949bcde9f5 Move project to Navidrome GitHub organization 2021-02-06 21:47:19 -05:00
Deluan
6ee45a9ccc Move project to Navidrome GitHub organization 2021-02-06 21:46:35 -05:00
Deluan
bc609be3c9 Fix space hotkey 2021-02-05 13:10:58 -05:00
Deluan
4b373560c6 Do not trigger next/prev event handlers when Cmd (meta) is pressed 2021-02-05 13:03:36 -05:00
Deluan
bb6197360b Upgrade to latest go-chi 2021-02-05 12:13:35 -05:00
Deluan
2f4a5fd9ae Fix test suite name 2021-02-04 15:44:44 -05:00
Deluan
26f838167e Add tests to diode 2021-02-04 15:42:08 -05:00
Deluan
c7af3b8256 Add test to Event 2021-02-04 12:43:46 -05:00
Deluan
7adacbac0d Removed event.type from SSE, as it was causing the browser to hang.
Needs more investigation, but for now, back to the simple message format
2021-02-04 12:37:06 -05:00
Deluan
77fc4841e4 Remove option to download discs of a set 2021-02-03 23:05:15 -05:00
Aries
f43999842f Update Japanese translation (#757)
* Fix ja translation

* Fix japanese translation
2021-02-03 22:08:11 -05:00
Deluan
64b22688ba Fix Portuguese transaltion 2021-02-03 19:27:14 -05:00
Deluan
e9dad3dd67 Update Portuguese transaltion 2021-02-03 19:13:44 -05:00
Deluan
847531391d Help dialog with available hotkeys 2021-02-03 19:08:03 -05:00
Deluan
a168f46b95 Better hotkey organization 2021-02-03 18:29:33 -05:00
Deluan
22145e070f Replace custom chunking logic with a utils.BreakUpStringSlice call 2021-02-03 17:26:03 -05:00
Deluan Quintão
9a3e75be00 Add tests with GoLang 1.16-RC to the pipeline 2021-02-03 14:39:59 -05:00
Deluan
618d5fc81f Better duration formatting in logs 2021-02-03 13:04:20 -05:00
Deluan
9668263235 Logging when triggering manual scan 2021-02-03 00:27:59 -05:00
Deluan
9959862791 Replace react-hotkeys-hook with react-hotkeys 2021-02-02 23:13:18 -05:00
Deluan
8e02659441 Do not sanitize Album comments. This was already handled in the backend, when importing. Fix #715 2021-02-02 19:08:00 -05:00
Deluan
905c685696 Use diodes instead of channels in SSE broker 2021-02-02 18:55:08 -05:00
Deluan
591a5344ac Workaround to remember logarithmic volume 2021-02-02 18:29:20 -05:00
Deluan
e79922def1 Fix React "unique key prop" warning 2021-02-02 18:00:06 -05:00
Deluan
a3eb14d7f4 Fix displaying album year when viewing an artist's albums 2021-02-02 17:49:11 -05:00
Deluan
3b52df5efd Update golangci-lint in the pipeline 2021-02-01 16:54:40 -05:00
Deluan
031756caf6 Update canisue-lite 2021-02-01 16:45:33 -05:00
Deluan
70470e4c81 Increase heap memory for JS job 2021-02-01 16:40:55 -05:00
Deluan
58a52c31c2 Turn off memory profiling, saving a couple of megabytes 2021-02-01 16:30:06 -05:00
Deluan
1f3bc4d202 Use tools.go commands without installing 2021-02-01 16:16:30 -05:00
Deluan
950ba9f77e Bump github.com/google/wire from 0.4.0 to 0.5.0 2021-02-01 08:54:35 -05:00
Deluan
861b406575 Use new simplified uuid.NewString() syntax 2021-02-01 01:22:31 -05:00
Deluan
b47ec02f02 Reenable ImageCache and ActivityPanel as default 2021-02-01 00:31:02 -05:00
Deluan
7cc9fbaaf9 Revert: Use modified time as updated_at and created_at when refreshing/creating albums 2021-02-01 00:30:45 -05:00
Deluan
9807b0b6c0 Use modified time as updated_at and created_at when refreshing/creating albums. Closes #717 2021-01-31 22:17:40 -05:00
Deluan
1af78e9503 Build and publish Docker image for armv6 (closes #747) 2021-01-31 19:36:18 -05:00
Deluan Quintão
02c228da1b Update Translations (#751)
* Update zn.json (POEditor.com)

* Update cs.json (POEditor.com)

* Update da.json (POEditor.com)

* Update nl.json (POEditor.com)

* Update eo.json (POEditor.com)

* Update fr.json (POEditor.com)

* Update de.json (POEditor.com)

* Update it.json (POEditor.com)

* Update ja.json (POEditor.com)

* Update pl.json (POEditor.com)

* Update pt.json (POEditor.com)

* Update ru.json (POEditor.com)

* Update es.json (POEditor.com)

* Update th.json (POEditor.com)

* Update tr.json (POEditor.com)

* Update pt.json (POEditor.com)
2021-01-31 19:04:48 -05:00
Deluan
f7aa5c452c Add translation to skip_nav 2021-01-31 19:01:56 -05:00
Deluan Quintão
6c53ce4bb3 Update en.json (POEditor.com) 2021-01-31 18:57:07 -05:00
Deluan
f325907da4 Upgrade react-admin to 3.12.0 2021-01-31 18:50:27 -05:00
Deluan
2c89e0dc13 Bump react-drag-listview from 0.1.7 to 0.1.8 in /ui 2021-01-31 18:30:26 -05:00
Deluan
7d23eca721 Bump @testing-library dependencies 2021-01-31 18:26:25 -05:00
Deluan
77705e4d79 Upgrade react-jinke-music-player to 4.21.2
Enable fadeIn/fadeOut when pausing/playing and logarithmic volume (fix #668)
2021-01-31 18:23:32 -05:00
Deluan
afbd9a3b37 Bump github.com/google/uuid from 1.1.2 to 1.2.0 2021-01-31 17:56:06 -05:00
Deluan
41ec44ae1b Bump github.com/pressly/goose from 2.6.0+incompatible to 2.7.0+incompatible 2021-01-31 17:48:08 -05:00
Deluan
dc8051ed53 Bump github.com/golangci/golangci-lint from 1.33.0 to 1.36.0 2021-01-31 17:43:03 -05:00
Deluan
c5686c4884 Replace periodic scanner cancellation channel with a context 2021-01-31 17:37:54 -05:00
Deluan
9520c30c32 Fix "failed" Subsonic response. Fix #716 2021-01-07 08:24:13 -05:00
Deluan
069199b2d8 Removed invalid comment 2021-01-03 18:16:37 -05:00
Deluan
05ffdede56 Bump react-hotkeys-hook from 2.4.0 to 3.0.0 in /ui 2021-01-03 18:05:31 -05:00
Deluan
b12b3c49bd Bump @material-ui/lab from 4.0.0-alpha.56 to 4.0.0-alpha.57 2021-01-03 18:00:39 -05:00
Deluan
0f29da966e Bump @testing-library/user-event from 12.5.0 to 12.6.0 in /ui 2021-01-03 17:56:25 -05:00
Deluan
81d9750fa6 Bump react-icons from 3.11.0 to 4.1.0 in /ui 2021-01-03 17:52:53 -05:00
dependabot-preview[bot]
0af54e43dc Bump uuid from 8.3.1 to 8.3.2 in /ui
Bumps [uuid](https://github.com/uuidjs/uuid) from 8.3.1 to 8.3.2.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/master/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v8.3.1...v8.3.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-01-02 13:26:27 -05:00
Steve Richter
0d35148152 Check for window.isSecureContext when determining Notification support 2020-12-28 20:46:11 -05:00
Deluan
7c23bd0890 Fix log message, as it is also used for taglib 2020-12-25 12:45:38 -05:00
Deluan
10e52bdd3f Use order_* fields for sorting by album and artist 2020-12-25 12:37:16 -05:00
Deluan
9e84ce42b5 Use same album songs order for UI and Subsonic API 2020-12-25 12:37:16 -05:00
lbonn
15b289201a Fall back to media file path when sorting
If files cannot be sorted by disc and track id, try by artist then
title.

One use case is a loose compilation of files with same album, album
artist, and no track numbers. File order was then undetermined, in
practice depended on insertion order in the database.
2020-12-25 12:37:16 -05:00
Deluan
cd1c693a23 Remove superfluous argument 2020-12-24 10:39:10 -05:00
Deluan
2073871fa1 Use netgo (instead of C bindings). Fix #700 2020-12-23 15:29:18 -05:00
Deluan
dab83c4f6a Disambiguate tracks by AlbumArtist when sorting by album 2020-12-23 11:38:40 -05:00
Deluan
db5b9246dd Handle more sort/order cases 2020-12-23 11:37:38 -05:00
Deluan
cdae4347a6 Make ServerStart variable global 2020-12-21 11:39:38 -05:00
Deluan
8c063c4f0c Removed unused variable 2020-12-21 10:01:37 -05:00
Deluan
14b060a42a Only close connection if the write times out 2020-12-20 15:21:46 -05:00
Deluan
1804fb3e50 Fix duration formatting, add days and don't show 60 seconds 2020-12-20 13:29:09 -05:00
Deluan
ea2f94658a Error should always be nil 2020-12-20 13:28:33 -05:00
Deluan
4b38a13243 Make event handlers naming consistent (camelCase) 2020-12-20 13:28:11 -05:00
Deluan
29817db9f2 Simplify worker pool 2020-12-15 20:48:06 -05:00
dependabot-preview[bot]
fc4ddee122 Bump github.com/onsi/gomega from 1.10.3 to 1.10.4
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.10.3 to 1.10.4.
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.10.3...v1.10.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-12-14 08:06:22 -05:00
Deluan
a241865209 Add elapsed time when scanner does not detect any new changes 2020-12-13 20:48:16 -05:00
Deluan
ea09629803 Fix another possible race condition 2020-12-13 20:17:20 -05:00
Deluan
f2fd7ed016 Fix race condition that could cause multiple EventSource connections from the same client 2020-12-13 14:44:57 -05:00
Deluan
4f90fa9924 Add denormalized list of artist_ids to album, to speed-up artist's albums queries
This will be removed once we have a proper many-to-many relationship between album and artist
2020-12-13 14:05:48 -05:00
Deluan
f86bc070de Don't break on login when activity panel is disabled 2020-12-13 12:16:02 -05:00
Deluan
1d338417e9 Make done channel buffered 2020-12-13 11:58:00 -05:00
Deluan
d685aefab3 Don't ever stop the listen go routine 2020-12-12 23:04:50 -05:00
Deluan
e27d917bd4 Forgot to allocate done channel 2020-12-12 21:10:43 -05:00
Deluan
8b92796a5c Disconnect the client if the output buffer fills up 2020-12-12 18:26:30 -05:00
Deluan
17833cd9d2 Make names more consistent 2020-12-12 13:46:36 -05:00
Deluan
e2969aa34c Use non-blocking event sending 2020-12-12 13:35:49 -05:00
Deluan
500da8bc7b Bump react-icons from 3.11.0 to 4.1.0 2020-12-11 12:21:53 -05:00
Deluan
db3b53ff43 Bump prettier from 2.1.2 to 2.2.1 2020-12-11 12:13:21 -05:00
Deluan
291d28887b Bump @testing-library dependencies 2020-12-11 12:10:46 -05:00
Deluan
3c6b8d18cd Bump golangci-lint from 1.32.2 to 1.33.0 2020-12-11 11:40:06 -05:00
Deluan
0111d3ae60 Bump react-admin from 3.10.2 to 3.10.4 2020-12-11 11:34:25 -05:00
Deluan
0cde8cbf2e Fix logging field case 2020-12-11 11:26:06 -05:00
dependabot-preview[bot]
b08eac54fd Bump github.com/Masterminds/squirrel from 1.4.0 to 1.5.0
Bumps [github.com/Masterminds/squirrel](https://github.com/Masterminds/squirrel) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/Masterminds/squirrel/releases)
- [Commits](https://github.com/Masterminds/squirrel/compare/v1.4.0...v1.5.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-12-10 16:04:29 -05:00
dependabot-preview[bot]
f7411558e3 [Security] Bump ini from 1.3.5 to 1.3.7 in /ui (#686)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7. **This update includes a security fix.**
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-12-10 15:38:44 -05:00
Deluan
a74b365feb Only adds route to /events if Activity Panel is enabled 2020-12-09 15:33:37 -05:00
Deluan
350f1dc951 Docker run does not need to be interactive for building snapshots 2020-12-07 13:37:22 -05:00
Deluan
25ae1c6cdd Return album art as a Reader 2020-12-02 09:13:36 -05:00
Deluan
0aaa261a71 Don't show warning about image cache disabled if pre-cache warmer is disabled 2020-12-02 08:52:35 -05:00
Deluan Quintão
f2a8308925 Update translations (#673)
* Update zn.json (POEditor.com)

* Update nl.json (POEditor.com)

* Update de.json (POEditor.com)

* Update ja.json (POEditor.com)
2020-12-01 16:34:50 -05:00
Deluan
240149cda4 DIsable Image Cache by default.
See: https://github.com/deluan/navidrome/issues/446#issuecomment-736574447
2020-12-01 18:01:16 +00:00
Deluan
ae58ac6a6c Add configuration for VSCode's Remote Container development 2020-12-01 17:57:29 +00:00
Deluan
a8c5fa6d49 Fix file descriptor leak in SSE implementation.master
See https://github.com/deluan/navidrome/issues/446#issuecomment-736296465
2020-12-01 09:24:44 -05:00
Deluan
9414ce6549 Bump react-admin to 3.10.2 2020-11-29 22:55:07 -05:00
Deluan
7bd31da0d5 Fix console warning about required property 2020-11-29 20:31:07 -05:00
Deluan
975579ab26 Add option for player to report real paths in Subsonic API. Closes #625 2020-11-28 10:25:23 -05:00
Deluan
7becc18da9 Don't explode when record is not loaded yet 2020-11-28 09:44:07 -05:00
Deluan
4ca98fb827 Add hotkey s to toggle star.
Refers to #585
2020-11-28 00:52:38 -05:00
Deluan
aae66cfcf0 Always show context menu if not in desktop 2020-11-27 23:52:23 -05:00
Deluan
2010fcf4ca Remove possible undefined error 2020-11-27 18:53:25 -05:00
Deluan
2ffb28fc2d Replace classnames with clsx 2020-11-27 18:27:32 -05:00
Deluan
0b729e1cf9 Hide star completely if in Playlist view 2020-11-27 16:24:22 -05:00
Deluan
ab856e3dd1 Wrap comment text. Fixes #637 2020-11-27 16:02:02 -05:00
Deluan
90c407b7f6 Also use PureDatagridRow to speed up SongDatagrid 2020-11-27 14:24:22 -05:00
Deluan
f7d1b80b69 Simplify AudioTitle on mobile view 2020-11-27 13:30:51 -05:00
Deluan
2b95422e88 Make "star" column aligned with context menu in Album List view 2020-11-27 13:13:51 -05:00
Deluan
7d075b1882 Make SongDatagrid faster by using PureDatagridBody 2020-11-27 13:13:51 -05:00
Deluan
0e9b0d466c Hide row when reordering playlist 2020-11-27 13:13:51 -05:00
Deluan
e5c7819586 Fix playlists 2020-11-27 13:13:51 -05:00
Deluan
a42fb024be Fix song context menu "on hover" visibility 2020-11-27 13:13:51 -05:00
Deluan
f5808288ab Fix Album View 2020-11-27 13:13:51 -05:00
Deluan
3209430ebd Fix artists 2020-11-27 13:13:51 -05:00
Deluan
d9893cf84d Bump React-Admin to 3.10.1 2020-11-27 13:13:51 -05:00
Deluan
9064697123 Remove stray console.log 2020-11-27 13:13:51 -05:00
Deluan
b6c578e3a2 Change format of events sent by server, leveraging event type and id 2020-11-25 20:46:21 -05:00
Deluan
cc5eaf4caf go mod tidy 2020-11-25 19:09:08 -05:00
Deluan
f29bb211d1 Better termination handling in Scanner's progress 2020-11-25 19:05:36 -05:00
Deluan Quintão
3ad36ebd2a Update translations (#644)
* Update eo.json (POEditor.com)

* Update fr.json (POEditor.com)

* Update pt.json (POEditor.com)
2020-11-25 15:58:45 -05:00
Deluan
31d6508b1d Remove reactjkplayer's audioTitle, as it is not used and only causes warnings in the console 2020-11-25 15:37:10 -05:00
Steve Richter
bc72f41180 Adjust AudioTitle in Player
- Show info on 2 lines
- Show album
2020-11-25 15:37:10 -05:00
Deluan
63171368ed Disable Activity Panel by default.
You'll need to set `DevActivityPanel` (or `ND_DEVACTIVITYPANEL`) to `true` to re-enable it
2020-11-25 15:29:46 -05:00
Deluan
5137407377 Add "keepalive" resource. It was causing issues in Firefox when using the dataProvider 2020-11-23 21:28:09 -05:00
Deluan
92ba658606 Don't panic if log parameters are invalid 2020-11-22 17:12:53 -05:00
Zane van Iperen
763a3ef267 Fix startup failure when image cache is disabled.
Fixes #655
2020-11-22 17:03:09 -05:00
Deluan
a89afb5fcf Fix aspect ratio in Album show view 2020-11-22 15:03:41 -05:00
Jason Walton
69b2fe92f5 Fix aspect ratio for non-square album art. 2020-11-22 15:03:41 -05:00
stefanomarty
3996764486 Update it.json
Just a correction to a few mistypings.
2020-11-21 18:14:44 -05:00
Deluan
a288e7e858 Allow the NotificationToggle to be in sync with the selected option in the browser 2020-11-21 02:03:54 -05:00
Steve Richter
14525cd056 Fix formatting 2020-11-21 02:03:54 -05:00
Steve Richter
2397a7e464 Add Desktop Notifications 2020-11-21 02:03:54 -05:00
Steve Richter
b8d47d1db4 Fix default getPerPage for 'md' widths 2020-11-21 01:34:36 -05:00
Deluan
48a6ba2956 Bump GoLang to 1.15.5 2020-11-20 21:55:03 -05:00
Deluan
3e8bee4f65 Make eventStream connection/reconnection more reliable
Also more logs on the server
2020-11-20 20:27:30 -05:00
Deluan
c8c95bfb47 Remove React console warning 2020-11-20 19:59:54 -05:00
Deluan
666b058ce4 Request album covers when DevFastAccessCoverArt is true 2020-11-18 16:59:06 -05:00
JG
d6066c514d Updated spanish translation nov 2020 (#642)
* Updatind translation

* Updatind translation

* Update spanish translation

Co-authored-by: Gosz <gosh@4geeksmx.com>
2020-11-18 16:58:57 -05:00
Deluan
3c4903bc4e No need to create a new instance of the Artwork service 2020-11-17 12:16:13 -05:00
Deluan
af4609727c Goto album page when clicking on player's album cover 2020-11-17 10:33:53 -05:00
Deluan
53b2cdd33d Update Thai language 2020-11-16 16:49:13 -05:00
Deluan
088af9004a Only try to check cover art file for lastUpdated if fast access is not set 2020-11-16 16:39:31 -05:00
Deluan
1ee39835dd Retry connecting to the events endpoint more frequently on first load 2020-11-16 15:38:03 -05:00
Deluan Quintão
972a94dbf0 Update translations (#623)
* Update fr.json (POEditor.com)

* Update de.json (POEditor.com)

* Update es.json (POEditor.com)

* Update tr.json (POEditor.com)

* Update ru.json (POEditor.com)

* Add Thai translation, thanks to AZ11244

* Update cs.json (POEditor.com)

* Update zn.json (POEditor.com)

* Update da.json (POEditor.com)

* Update nl.json (POEditor.com)

* Update it.json (POEditor.com)

* Update th.json (POEditor.com)
2020-11-16 15:22:37 -05:00
Deluan
b87f7b6126 Bump react-redux from 7.2.1 to 7.2.2 2020-11-16 15:09:25 -05:00
Deluan
f09a6423f7 Bump react-dom from 16.13.1 to 16.14.0 2020-11-16 15:08:02 -05:00
Deluan
49d28d34b4 Bump jwt-decode from 3.0.0 to 3.1.2 2020-11-16 15:06:57 -05:00
Deluan
8b09f0369c Bump react-ga from 3.1.2 to 3.2.1 2020-11-16 15:03:16 -05:00
Deluan
41cbd3aba7 Bump @testing-library dependencies 2020-11-16 15:01:52 -05:00
Deluan
a1dcb9a4e3 Show folders scanned instead of files scanned 2020-11-16 00:36:12 -05:00
Deluan
be715c3696 Disable scan buttons when there's a scan in progress 2020-11-15 23:13:59 -05:00
Deluan
fddded3260 Move star to end of album title. Use flex for album details 2020-11-15 22:00:40 -05:00
Deluan
cf90f0a245 Update sizes with SQL, instead of a full rescan 2020-11-15 19:26:16 -05:00
Deluan
8bfaa0ad9d Better detection of ID fields, to use = instead of LIKE 2020-11-15 18:24:13 -05:00
Deluan
15697a6fa2 Bump github.com/golangci/golangci-lint from 1.32.1 to 1.32.2 2020-11-14 21:37:08 -05:00
Deluan
bcb3e1479f Bump github.com/astaxie/beego from 1.12.2 to 1.12.3 2020-11-14 21:35:39 -05:00
Deluan
44d13bd37c Remove stray Printf 2020-11-14 13:38:31 -05:00
Aries
9629c26537 Fix ja translation (#624) 2020-11-14 11:15:21 -05:00
Deluan
1c7f859b5e Add more broker log 2020-11-14 00:44:58 -05:00
Deluan
8b2a550368 Fix test 2020-11-14 00:25:25 -05:00
Deluan
b0ea517fdd Add Album comment to Album details 2020-11-14 00:13:43 -05:00
Deluan
08f96639f4 Add Uptime to Activity Panel 2020-11-13 20:09:23 -05:00
Deluan
b64bb706f7 Use Gravatar in GetAvatar Subsonic API 2020-11-13 14:57:49 -05:00
Deluan
a4ef31251d Enable activity panel by default 2020-11-13 13:44:38 -05:00
Deluan
84cd6b7f34 Add Esperanto translation, thanks to @ebanDev 2020-11-13 12:56:30 -05:00
Deluan
df86a8153e New translated terms 2020-11-13 12:51:32 -05:00
Deluan
b38be69b14 Make AppBar back to original height 2020-11-13 10:14:01 -05:00
Deluan
48e0d2c99e Trunc long names 2020-11-13 09:33:56 -05:00
Deluan
3dac9ae666 Fix linting error 2020-11-13 00:44:26 -05:00
Deluan
9d7995fd4d Redesign UserMenu, now with support for Gravatar 2020-11-13 00:40:30 -05:00
Deluan
7efc32d136 Ignore "Cover (front)" tag when using ffmpeg extractor 2020-11-12 23:17:06 -05:00
Deluan
153cf8f5af Don't display "Comment" field in details if it is empty 2020-11-12 22:01:59 -05:00
Deluan
b3f373cdb4 Better Activity panel layout 2020-11-12 21:57:28 -05:00
Deluan
08399c4854 Fix some JS console errors 2020-11-12 20:51:26 -05:00
Deluan
25db696c06 Detect different discs, even when missing the first track of the disc. Fix #620. 2020-11-12 20:33:20 -05:00
Deluan
bdad927f11 Fix color of activity icon on light themes 2020-11-12 18:19:54 -05:00
Deluan
b1a9dfee13 Replace <hr/> with Material-UI's <Divider/> 2020-11-12 17:08:20 -05:00
Deluan
c09ba509b2 Fine tune scan status behaviour 2020-11-12 16:12:31 -05:00
Deluan
0e7163eb2c Sanitize comments and lyrics on import, as they are rendered as HTML on the UI 2020-11-11 12:26:47 -05:00
Deluan
5111cf8c33 Display comments in SongDetails and AlbumList's details 2020-11-11 11:58:03 -05:00
Deluan
98af68ac99 Import comments and lyrics 2020-11-11 10:43:17 -05:00
Deluan
aee4eb71c4 Add support for multi-line tags 2020-11-11 09:45:46 -05:00
Deluan
99d454d8b0 Fix import 2020-11-10 20:51:43 -05:00
Deluan
11012302fd Add tests for formatters 2020-11-10 20:45:04 -05:00
Deluan
9d2426a601 Use a better notation for exporting JS components and functions 2020-11-10 19:27:28 -05:00
Deluan
8a44f61189 Fix setting up Event Stream message handler on first login 2020-11-10 16:53:09 -05:00
Deluan
7afad2c96e Fix download single track from Playlist 2020-11-10 16:24:34 -05:00
Deluan
08e63c867b Add config option to globally enable/disable downloads 2020-11-10 16:14:43 -05:00
Deluan
bf69c5589f Fix log message 2020-11-10 14:46:12 -05:00
Deluan
714100e24b Remove old TODO 2020-11-09 19:50:14 -05:00
Deluan
8f2fe6f9fa Add buffer to broker SendMessage 2020-11-09 19:24:27 -05:00
Deluan
08dbf44529 Better broker logging 2020-11-09 19:24:04 -05:00
Deluan
84080a0e44 Hide activity menu from non-admin users 2020-11-09 16:12:50 -05:00
Deluan
1b624b2505 Do not create the EventStream if unauthenticated 2020-11-09 16:12:50 -05:00
Deluan
a2e76d6898 Add flag to enable activity menu 2020-11-09 16:12:50 -05:00
Deluan
56803d0151 Auto-reconnect to event stream after 20secs timeout 2020-11-09 16:12:50 -05:00
Deluan
2b1a5f579a Adding a communication channel between server and clients using SSE 2020-11-09 16:12:50 -05:00
Deluan
3fc81638c7 Moved all reducers and actions to their own folders 2020-11-08 13:19:38 -05:00
Deluan
24b040adf9 Add more keyboard shortcuts
- : volume down
= : volume up
m : toggle sidebar menu

Refers to #585
2020-11-07 23:11:57 -05:00
Deluan
8d608ac5b2 Faster display of cover in album detail view 2020-11-07 22:45:04 -05:00
Deluan
02160465a5 Remove unused file 2020-11-07 12:46:52 -05:00
Deluan
b5abd80927 Update react-jinke-music-player to 4.9.1. Fix #568 2020-11-07 12:20:42 -05:00
Deluan
6542842938 Make sure we don't get durations with decimals 2020-11-05 18:27:46 -05:00
Deluan
8d7931b3bc Fix "seekable" log message (was always "false") 2020-11-05 18:11:12 -05:00
Deluan
9224a67a7b Add <- and -> hotkeys, to jump to previous or next song
Refers to #585
2020-11-05 17:38:53 -05:00
Deluan
873cea4046 Fix "Something went wrong" error when deleting a playlist 2020-11-05 14:06:21 -05:00
JorisL
0b977df8dd Fixed duration formatter to properly count lengths longer than 24 hours (#612)
* Fixed durationfield formatter to properly count lengths longer than
24 hours.

* formatted DurationField.js with npm prettier
2020-11-05 14:02:09 -05:00
Deluan
fb1461fd0b Fix reading dirs from a MergeFS 2020-11-05 13:36:10 -05:00
Deluan
9cbeddae8f Avoid cross-site scripting
See: https://lgtm.com/rules/1510377426397/
2020-11-05 12:32:39 -05:00
Deluan
c9b119f0a4 Make scrobble submits compatible with Last.FM specification
See https://github.com/deluan/navidrome/issues/18#issuecomment-656977060
2020-11-04 23:51:48 -05:00
Deluan
a6bd9f627e Make new cache layout the default 2020-11-04 23:25:38 -05:00
Deluan
861c742b3e Move notifications to the top
This avoids notifications getting covered by the player
2020-11-04 19:29:55 -05:00
Deluan
36596d4fdb Don't send the transcoded file if it is a HEAD request 2020-11-03 16:06:02 -05:00
Deluan
94f28f6216 Generate a better salt for Subsonic token authentication 2020-11-03 15:13:40 -05:00
Deluan
2f56f1b178 Use new fscache's SetKeyMapper
See a0daa9e527
2020-11-03 12:52:44 -05:00
Deluan
f4a88b8319 Update screenshots 2020-11-03 09:23:04 -05:00
Deluan
f50aeb0b21 Bump golangci-lint version in pipeline 2020-11-02 20:56:52 -05:00
Deluan
fd1604b1d2 Add user's name to UserMenu 2020-11-02 17:13:12 -05:00
Deluan
7fbdcf8ddc Upgrade react-admin to 3.9.6 2020-11-02 17:12:52 -05:00
Deluan
7f7b0c1f0d Move Settings options to UserMenu 2020-11-02 16:57:21 -05:00
Deluan
68e0fe574f Bump github.com/golangci/golangci-lint from 1.32.0 to 1.32.1 2020-11-02 11:45:58 -05:00
Deluan Quintão
8ddf4d62af Update README.md 2020-11-02 11:43:05 -05:00
Deluan
9bcd606fe8 Fix Artist full_text refresh 2020-11-02 10:27:01 -05:00
Deluan
7819e834c8 Fix Artist filtering 2020-11-02 09:58:51 -05:00
Deluan
779d4a1c85 Revert "Process empty folders as changed folders"
This reverts commit e07152b695.
2020-11-02 07:57:47 -05:00
Deluan
e07152b695 Process empty folders as changed folders
This is a workaround for rclone not changing the directory modtime when you delete all folders from it (happens when you are moveing things around with beets)
2020-11-01 23:25:34 -05:00
Deluan
ee5a0698c0 Simplify scanner utilization 2020-11-01 18:37:17 -05:00
Deluan
71b77cba2b Bump Subsonic API to 1.16.1 2020-11-01 17:04:53 -05:00
Deluan
8e584ee020 Update count on getScanStatus 2020-11-01 16:54:33 -05:00
Deluan
3ea5b85b36 go mod tidy 2020-11-01 14:40:48 -05:00
Deluan
cfad35544b Add artistImageUrl available in getArtists endpoint
Also cache artist info in the DB for 1 hour
2020-11-01 14:37:29 -05:00
dependabot-preview[bot]
7583ddac65 Bump github.com/golangci/golangci-lint from 1.31.0 to 1.32.0
Bumps [github.com/golangci/golangci-lint](https://github.com/golangci/golangci-lint) from 1.31.0 to 1.32.0.
- [Release notes](https://github.com/golangci/golangci-lint/releases)
- [Changelog](https://github.com/golangci/golangci-lint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/golangci/golangci-lint/compare/v1.31.0...v1.32.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-01 14:37:08 -05:00
Deluan
6b89679e08 Remove hardcoded color 2020-10-31 19:40:25 -04:00
Deluan
3535fba9dd Fix BulkActions contrast once and for all(?) 2020-10-31 18:46:14 -04:00
Deluan
488db26675 Improve bulk actions color contrast 2020-10-31 11:05:33 -04:00
Deluan
1f842b08e2 Remove duplicated code for SongBulkActions 2020-10-31 10:46:38 -04:00
Deluan
aabef62b11 Add "PlayNow" button to bulk actions 2020-10-31 10:14:12 -04:00
Deluan
6c0778a867 Add log to pool 2020-10-31 01:13:36 -04:00
Deluan
58d6b0a84f Cache Warmer now waits for Cache to be available 2020-10-31 00:40:21 -04:00
Deluan
145a5708ca Stop tag_scanner when waltDirTree is interrupted by errors
Otherwise, tag_scanner remove tracks from folders that would come after the error
2020-10-31 00:06:28 -04:00
Deluan
6ccdc2e068 Fine tune colors, remove PlayButton from AlbumDetail 2020-10-30 19:39:47 -04:00
Chris Newton
6da2f1ba92 feat: ran prettier over changes 2020-10-30 19:39:47 -04:00
Chris Newton
28bcd3f99e feat: fixed linting error 2020-10-30 19:39:47 -04:00
Chris Newton
1076dda011 feat: changed hvoer state for album list, added play icon to album details 2020-10-30 19:39:47 -04:00
Chris Newton
e30704fe0f feat: altered grid layout to be more like itunes 2020-10-30 19:39:47 -04:00
Deluan
84384da8d1 Better naming for function 2020-10-30 13:14:53 -04:00
Victorhck
62fe1cdc43 improve spanish translation 2020-10-30 10:05:16 -04:00
Deluan
4d6c9482ff Recover from panic when reading invalid id2 tags
Workaround for #596
2020-10-30 09:53:38 -04:00
Deluan
cdd44a2830 Abort scan when media folder is empty
This is to prevent all data being deleted in the case where a mount is not available
2020-10-30 09:39:36 -04:00
Deluan
ba8d2f5da8 Log when a cache has finished loading 2020-10-30 00:33:39 -04:00
Deluan
00ec6cf042 Process changed folders as they are discovered 2020-10-29 23:47:43 -04:00
Deluan
2f394623c8 WIP 2020-10-29 23:19:26 -04:00
Deluan
f1a24b971a Use timestamp of artwork file instead of album's UpdatedAt in the cache key 2020-10-29 23:19:26 -04:00
Deluan
d913108de2 Add option to disable track cover art. Should help with cloud mounting (rclone) 2020-10-29 10:57:33 -04:00
Deluan
32bac11b61 Make CreatePlaylist response compatible with API >1.14.0 2020-10-28 12:46:06 -04:00
Deluan
78630d427d Limit startScan to admins only 2020-10-27 20:22:05 -04:00
Deluan
1e57852eff Make pool's queue buffered. Workaround while we don't put the queue in disk 2020-10-27 20:12:27 -04:00
Deluan
464e251d19 Only start the cache warming after all folders were scanned 2020-10-27 20:11:25 -04:00
Deluan
d9f7a154cf Implements library scanning endpoints. Also:
- Bumped Subsonic API version to 1.15:
- Better User/Users Subsonic endpoint implementations, not final though
2020-10-27 18:20:50 -04:00
Deluan
9b756faef5 Make caches singletons 2020-10-27 18:20:50 -04:00
Deluan
515528ee6d Disable flaky test 2020-10-27 16:07:53 -04:00
Deluan
4bd6012f11 Fix job dependencies in pipeline 2020-10-27 15:58:27 -04:00
Deluan
216491815c Increased pool test timeout (hate time based tests...) 2020-10-27 15:58:13 -04:00
Deluan
4777cf0aba Simplify error responses 2020-10-27 15:33:28 -04:00
Deluan
0f418a93cd Completely removed engine package, fewer abstraction layers \o/ 2020-10-27 15:27:37 -04:00
Deluan
d0bf37a8a9 Move mock datastore to tests package 2020-10-27 15:23:49 -04:00
Deluan
313a088f86 Make mocks strongly typed 2020-10-27 15:23:49 -04:00
Deluan
6152fadd92 Removed list_generator completely 2020-10-27 15:23:48 -04:00
Deluan
3037ea01e2 Removed more layers of indirection from the engine package 2020-10-27 15:23:48 -04:00
Deluan
acba4b16ee Add test for pool 2020-10-27 15:23:48 -04:00
dependabot-preview[bot]
8dfa929666 Bump github.com/kr/pretty from 0.2.0 to 0.2.1
Bumps [github.com/kr/pretty](https://github.com/kr/pretty) from 0.2.0 to 0.2.1.
- [Release notes](https://github.com/kr/pretty/releases)
- [Commits](https://github.com/kr/pretty/compare/v0.2.0...v0.2.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-10-27 00:15:32 -04:00
Deluan
c1fb32cedb Replace unicode quotes and dash with simple ascii chars
External services do not use these unicode chars. Ex:
- “Weird Al” Yankovic
- Bachman–Turner Overdrive
- The Go‐Go’s
are not found in Last.FM and Spotify
2020-10-27 00:13:39 -04:00
Deluan
b6a6422fac Upgrade GoLang to 1.15.3 2020-10-26 13:04:14 -04:00
Deluan
21ed7348c6 Remove invalid migration 2020-10-26 10:57:21 -04:00
Deluan
95cc211659 Revert "Make caches singletons" 2020-10-26 10:11:47 -04:00
Deluan
bf5318d776 Add flag to enable new cache layout 2020-10-26 09:54:36 -04:00
Deluan
81d7556cdf Make caches singletons 2020-10-25 23:22:52 -04:00
Deluan
1e56f4da76 Add simple cache warmer, disabled by default 2020-10-25 23:22:52 -04:00
Deluan
f3bb51f01b Add formatting to config dump 2020-10-25 23:22:52 -04:00
Deluan
197d4024f7 Add dedicated Item interface for cache items 2020-10-25 23:22:52 -04:00
Deluan
7eaa42797a Uses cached original image when requesting a resized image 2020-10-25 23:22:52 -04:00
Deluan
d39bd0219a Fix cache key-mapping 2020-10-25 23:22:52 -04:00
Deluan
9f533b2108 New Cache FileSystem implementation 2020-10-25 23:22:52 -04:00
Deluan
1cfa7b2272 Change MediaFolder.ID type to int32 2020-10-25 23:22:52 -04:00
Deluan
d24709b521 Add getScanStatus Subsonic response 2020-10-25 23:22:52 -04:00
Deluan
af7eaa2b7a Add scanner status 2020-10-25 23:22:52 -04:00
Deluan
c0ec0b28b9 Add better process lifecycle management 2020-10-24 22:43:59 -04:00
Deluan
6d08a9446d Fix test suite name 2020-10-23 21:43:33 -04:00
Deluan
04fd72e1fa Change avatar placeholder to new logo 2020-10-23 21:37:53 -04:00
Deluan
fc19199fbe Add Russian translation. Thanks @lun4r 2020-10-23 09:54:25 -04:00
Deluan
4514a54744 Fix ignoring hidden folders when scanning 2020-10-22 13:59:54 -04:00
Deluan
f9e0de31b8 Fix missing last.fm and spotify config keys. Closes #589 2020-10-22 08:31:47 -04:00
Deluan
1cd2f015c2 Get Similar Artists in parallel
Also don't fail `GetArtistInfo` when Last.FM is not configured
2020-10-21 21:44:03 -04:00
Deluan
ed84c5a0a3 Bump @testing-library/react from 11.0.4 to 11.1.0 in /ui 2020-10-21 18:24:20 -04:00
Deluan
b88f9013dc Fix getAlbumList.byYear. See https://github.com/daneren2005/Subsonic/issues/967 2020-10-21 17:32:10 -04:00
dependabot-preview[bot]
62ed30afed Bump @testing-library/user-event from 12.1.7 to 12.1.8 in /ui
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 12.1.7 to 12.1.8.
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/master/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v12.1.7...v12.1.8)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-10-21 17:10:39 -04:00
Deluan
6dc21d0595 Check for Last.FM and Spotify configuration at startup 2020-10-21 17:10:06 -04:00
Deluan
79710fbee0 go mod tidy 2020-10-21 16:58:55 -04:00
dependabot-preview[bot]
c89b89cd92 Bump react from 16.13.1 to 16.14.0 in /ui
Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) from 16.13.1 to 16.14.0.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/master/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v16.14.0/packages/react)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-10-21 16:58:07 -04:00
dependabot-preview[bot]
dcea5eb449 Bump github.com/spf13/cobra from 1.1.0 to 1.1.1
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Changelog](https://github.com/spf13/cobra/blob/master/CHANGELOG.md)
- [Commits](https://github.com/spf13/cobra/compare/v1.1.0...v1.1.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-10-21 16:51:55 -04:00
Deluan Quintão
b5c68c971d Update pl.json (POEditor.com) 2020-10-21 16:50:31 -04:00
Deluan Quintão
fe38f99739 Update nl.json (POEditor.com) 2020-10-21 16:50:31 -04:00
Deluan Quintão
ff3a89b15a Update cs.json (POEditor.com) 2020-10-21 16:50:31 -04:00
Deluan
078a7c24e6 Add userRating to Subsonic API's Artist 2020-10-21 15:51:12 -04:00
Deluan
69e1059705 Prefer starred and high rated versions for Top Songs 2020-10-21 15:11:26 -04:00
Deluan
075c28d2e5 Fix performance and precision of TopSongs 2020-10-21 14:07:01 -04:00
Deluan
a45b5a037f Match Top Songs by mbid, add indexes to media_file 2020-10-21 10:13:03 -04:00
Deluan
3cf8b8e97d Fix migration that adds MBIDs 2020-10-21 09:02:51 -04:00
Deluan
b93a3db267 Fix sort order for TopSongs 2020-10-21 00:10:46 -04:00
Deluan
53c1e9ec35 Include tracks in TopSongs where the requested artist is the album artist 2020-10-20 23:52:45 -04:00
Deluan
12cedee867 Prefer older versions on GetTopSongs 2020-10-20 23:46:45 -04:00
Deluan
2f11c2dc8f Bump Subsonic API compatibility to 1.13 2020-10-20 22:54:37 -04:00
Deluan
049ac70b2b Add "real" TopSongs 2020-10-20 22:53:52 -04:00
Deluan
b5e20c1934 Ignore invalid MBIDs (ex: discogs IDs) 2020-10-20 17:45:32 -04:00
Deluan
173dd52fe1 Use MBID with most occurrences 2020-10-20 17:16:24 -04:00
Deluan
6663c079e0 Add MBIDs to media_file, album and artist 2020-10-20 16:27:22 -04:00
Deluan
64ccb4d188 Add SimilarSongs functionality 2020-10-20 16:07:31 -04:00
Deluan
a289a1945f Remove redundant interfaces 2020-10-20 16:07:31 -04:00
Deluan
a257891b46 Get better artist images results 2020-10-20 16:07:31 -04:00
Deluan
40fd5bab34 Search for artists case-insensitive 2020-10-20 16:07:31 -04:00
Deluan
e9e09a7480 Add dedicated SimilarArtists call 2020-10-20 16:07:31 -04:00
Deluan
29d8950e5b Better ArtistInfo field names 2020-10-20 16:07:31 -04:00
Deluan
00b6f895bb Fix lint errors 2020-10-20 16:07:31 -04:00
Deluan
07d96f8308 Add missing fields to ArtistInfo 2020-10-20 16:07:31 -04:00
Deluan
07535e1518 Add ExternalInformation core service (not a great name, I know) 2020-10-20 16:07:31 -04:00
Deluan
19ead8f7e8 Add initial spotify client implementation 2020-10-20 16:07:31 -04:00
Deluan
eb74dad7cd Add initial last.fm client implementation 2020-10-20 16:07:31 -04:00
Deluan
61d0bd4729 Add support for WavPack files 2020-10-20 10:47:29 -04:00
Deluan
def5db9729 Update dependencies (go mod tidy) 2020-10-16 16:24:10 -04:00
dependabot-preview[bot]
3d11bdcfd1 Bump github.com/spf13/cobra from 1.0.0 to 1.1.0
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.0.0 to 1.1.0.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Changelog](https://github.com/spf13/cobra/blob/master/CHANGELOG.md)
- [Commits](https://github.com/spf13/cobra/compare/v1.0.0...v1.1.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-10-15 09:21:26 -04:00
Deluan
0ff89679ba Use new renderAudioTitle (to avoid the [Object object] song title on iOS) 2020-10-13 11:20:30 -04:00
Deluan
0c095f6d5d Upgrade ginkgo/gomega 2020-10-13 09:35:31 -04:00
Deluan
2f8dc794de Add and show Playlists sizes 2020-10-12 22:31:01 -04:00
Deluan
68a9be5e86 Add Artist (discography) size, and show sizes in Download caption 2020-10-12 22:31:01 -04:00
Deluan
1ffc8d619e Log ffmpeg detection as Info 2020-10-12 21:59:03 -04:00
Deluan
2de0a40c6f Fix Album size should be int64 2020-10-12 21:04:12 -04:00
Deluan
5417031d79 Update some GH actions 2020-10-12 12:21:01 -04:00
Deluan
ae817da223 Upgrade golangci-lint
- Fix a SQL string concatenation
- Install golangci-lint using `tools.go`
2020-10-12 12:21:01 -04:00
Jay R. Wren
fd6edf967f Add size to album details (#561)
* add size to album details

for #534

* addressing review comments:

* create index album(size)
* remove unneeded Size field from refresh struct
* add whitespace to album details
* add size to album list view

* prettier
2020-10-12 11:10:07 -04:00
Deluan
c60e56828b Fix ffmpeg detection 2020-10-12 10:59:42 -04:00
Deluan
edc9344327 Only link from current playing song title to album view if not in iOS.
Ideally the react-player should accept a Link as the audioTitle
2020-10-11 15:04:15 -04:00
Deluan
fea5d23fc7 Add ffmpeg detection at start-up 2020-10-06 17:24:16 -04:00
Deluan
26d2af17a3 Fix read DISCNUMBER as a DiscNumber tag in ffmpeg extractor 2020-10-06 17:06:47 -04:00
Gosz
f373f5f83e Updating spanish translation 2020-10-06 11:38:54 -04:00
Deluan
92b7ef40af Disable CSP for now 2020-10-06 11:24:59 -04:00
Deluan
39cb3455db Prepare for release: go mod tidy 2020-10-06 09:55:40 -04:00
Deluan Quintão
4ac4806bf8 Update fr.json (POEditor.com) 2020-10-06 09:33:59 -04:00
Deluan Quintão
a282f62395 Update zn.json (POEditor.com) 2020-10-06 09:33:59 -04:00
dependabot-preview[bot]
3aac03d253 Bump @testing-library/user-event from 12.1.6 to 12.1.7 in /ui
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 12.1.6 to 12.1.7.
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/master/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v12.1.6...v12.1.7)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-10-06 09:29:11 -04:00
Deluan
cd171c40cb Add secure middleware, with sensible values 2020-10-06 08:46:58 -04:00
dependabot-preview[bot]
78c40ab6b4 Bump jwt-decode from 2.2.0 to 3.0.0 in /ui
Bumps [jwt-decode](https://github.com/auth0/jwt-decode) from 2.2.0 to 3.0.0.
- [Release notes](https://github.com/auth0/jwt-decode/releases)
- [Changelog](https://github.com/auth0/jwt-decode/blob/master/CHANGELOG.md)
- [Commits](https://github.com/auth0/jwt-decode/compare/v2.2.0...v3.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-10-06 08:41:08 -04:00
Deluan
21f7c1906d Fix ByPath queries should not match partial filenames 2020-10-06 08:13:25 -04:00
dependabot-preview[bot]
23fe8cdee6 Bump uuid from 8.3.0 to 8.3.1 in /ui
Bumps [uuid](https://github.com/uuidjs/uuid) from 8.3.0 to 8.3.1.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/master/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v8.3.0...v8.3.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-10-06 08:12:57 -04:00
Deluan
af55b93ac8 Make taglib the default metadata extractor 2020-10-05 21:01:03 -04:00
Deluan
665b1f6898 Fix auto-imported playlists losing the "Public" status. Fix #479 2020-10-05 12:40:44 -04:00
Deluan
35f748e0fb Use logo on signup page 2020-10-04 18:29:46 -04:00
Deluan
fc7a027d59 Update mobile login screenshot 2020-10-04 18:23:48 -04:00
Deluan
38c1999fcd Use logo on login page. Closes #247 2020-10-04 11:47:25 -04:00
Deluan
180f1354fc Make package name compatible with version installed by make setup 2020-10-03 20:13:47 -04:00
Deluan
abd51b2156 Use Subsonic API to star/unstar
This removes the need to update the annotations on Put(model), removing complexity and making it less buggy
2020-10-03 20:08:51 -04:00
Deluan
47976e13b1 Create index to make sort by starred faster 2020-10-03 20:08:51 -04:00
Deluan
bbd4503ac8 Move tools installation to tools.go 2020-10-03 11:14:19 -04:00
Aries
b40df6380e Update Japanese translation (#544) 2020-10-03 09:56:10 -04:00
Deluan
2d036b5966 Small refactoring, simplify function 2020-10-02 22:36:46 -04:00
Deluan
f859772723 Remove dangling tracks after changing MusicFolder. Fix #445 2020-10-02 16:18:45 -04:00
Deluan
1be79fa945 Reload translations when reloading the app 2020-10-02 14:28:55 -04:00
Deluan
1825b29737 Better PT translation 2020-10-02 14:17:34 -04:00
Deluan Quintão
13f08d3eae Update translations (#543)
* Update zn.json (POEditor.com)

* Update cs.json (POEditor.com)

* Update nl.json (POEditor.com)

* Update fr.json (POEditor.com)

* Update de.json (POEditor.com)

* Update it.json (POEditor.com)

* Update ja.json (POEditor.com)

* Update pl.json (POEditor.com)

* Update pt.json (POEditor.com)

* Update tr.json (POEditor.com)

* Update es.json (POEditor.com)

* Fix translations
2020-10-02 12:18:00 -04:00
Deluan
52d8aaa865 Add about dialog, with version and helpful links 2020-10-02 12:03:19 -04:00
Deluan
8dfc259857 Serve robots.txt from root (http://server/robots.txt) 2020-10-02 10:15:19 -04:00
Deluan
deef8e162d Hide the player when queue is empty, instead of removing it from the DOM 2020-10-01 13:40:44 -04:00
Deluan
b18e3289fb Add StarButton to player 2020-10-01 13:40:44 -04:00
certuna
4d60f72b7e Update index.js
registers the default serverworker, required to be installable on desktop as a PWA - as far as I can test, it doesn't seem to break anything
2020-10-01 12:12:18 -04:00
Deluan
e0fa85be28 Avoid Bing bot to automatically add Navidrome to the MS Store
See: https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps-edgehtml/microsoft-store#criteria-for-automatic-submission
2020-10-01 12:09:56 -04:00
Deluan
5b167031d2 Enable PWA's when setting BaseURL 2020-10-01 12:04:38 -04:00
Deluan
cf8756b14b Unexport private function 2020-10-01 09:56:09 -04:00
certuna
03867bd8b2 Update manifest.json
added start_url (required)
fixed icon paths
2020-10-01 08:56:27 -04:00
Deluan
943f35f7a5 Update to GoLang 1.15 2020-10-01 08:41:11 -04:00
Deluan
ca283f45ea Update serviceWorker to the latest from create-react-app 2020-09-29 17:10:06 -04:00
Deluan
bf93b5614c Bump react-admin to 3.8.5 2020-09-29 16:33:36 -04:00
Deluan
377d8f6b87 Fix continuous loop when showing an album or playlist 2020-09-29 16:29:34 -04:00
dependabot-preview[bot]
1eb62ee671 Bump github.com/sirupsen/logrus from 1.6.0 to 1.7.0
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.6.0...v1.7.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-29 16:28:58 -04:00
Deluan
39c94d3cd9 Play/Pause current song with <Space> key 2020-09-28 19:05:19 -04:00
Deluan
3fa4ef0166 Fix low severity vulnerabilities (npm audit fix) 2020-09-28 18:36:25 -04:00
Deluan
9116529b6d Change favicon to new logo 2020-09-28 16:48:35 -04:00
Deluan
bd8b573743 Update react-jinke-music-player 2020-09-28 12:22:05 -04:00
Deluan
a65318a00a Fix 'Play Next' icon in AlbumSongBulkActions 2020-09-27 12:50:58 -04:00
Deluan
a817701ee8 Use new ci-goreleaser. Fix pipeline 2020-09-26 20:30:48 -04:00
Deluan
4a4a8aff34 Build ARM with armel instead of armhf. Fixes #525 2020-09-26 13:08:45 -04:00
Fernando Rios
80b8b69cee Fix compilation of C++ code on certain linux systems 2020-09-26 13:08:28 -04:00
Deluan
ab0e091736 Fix link to Artist's albums in mobile view 2020-09-25 16:48:31 -04:00
Deluan
e6d1e67297 Add more padding tertiary info and the star icon, in Mobile simple list views. Fixes #466 2020-09-24 21:29:26 -04:00
Deluan
a99924ea20 Converted pre-push hook into a make target, avoid calling tests twice when releasing 2020-09-24 17:24:31 -04:00
Deluan
27adb84177 Add "Close" icon to player 2020-09-24 13:34:17 -04:00
Deluan
7a3bd935c2 Change default Opus transcoding format name to opus. Closes #521 2020-09-24 12:27:13 -04:00
Deluan
cff5c1ee53 Start player in pause mode if windows is reloaded/refreshed. Fixes #457 2020-09-24 11:12:47 -04:00
Deluan
fd32a28788 Fix JS linting error 2020-09-24 09:58:01 -04:00
Deluan Quintão
4f25e9ebf4 Update pl.json (POEditor.com) 2020-09-24 09:51:11 -04:00
Deluan Quintão
514117a477 Update ja.json (POEditor.com) 2020-09-24 09:51:11 -04:00
Deluan Quintão
07e8f41849 Update it.json (POEditor.com) 2020-09-24 09:51:11 -04:00
Deluan Quintão
133626dcd0 Update fr.json (POEditor.com) 2020-09-24 09:51:11 -04:00
Deluan Quintão
56a6fb91ab Update cs.json (POEditor.com) 2020-09-24 09:51:11 -04:00
Deluan Quintão
6e518d90d5 Update zn.json (POEditor.com) 2020-09-24 09:51:11 -04:00
Deluan Quintão
96b94106e6 Update en.json (POEditor.com) 2020-09-24 09:49:42 -04:00
Deluan
a1c670b40d Start player in pause mode if windows is reloaded/refreshed. Fixes #457 2020-09-24 09:40:04 -04:00
certuna
2230a9052f Update manifest.json
Updated name/description/colours in the manifest
2020-09-24 08:51:35 -04:00
Gosz
9b1be35c14 Updatind translation 2020-09-24 08:50:05 -04:00
Deluan
afe5a5b32a Fix extracting tags with spaces in the tagname ("Ex: Album Artist") 2020-09-22 14:42:36 -04:00
dependabot-preview[bot]
9edd7e9025 Bump @testing-library/user-event from 12.1.5 to 12.1.6 in /ui
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 12.1.5 to 12.1.6.
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/master/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v12.1.5...v12.1.6)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-22 09:36:13 -04:00
dependabot-preview[bot]
2be9a7dbec Bump prettier from 2.1.1 to 2.1.2 in /ui
Bumps [prettier](https://github.com/prettier/prettier) from 2.1.1 to 2.1.2.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.1.1...2.1.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-22 09:32:25 -04:00
Deluan
7305e3aa17 Add "Play Next" action (finally) 2020-09-21 20:10:52 -04:00
Deluan
aa133e6b00 Upgrade react-music-player to 4.18.2 2020-09-21 15:24:15 -04:00
JG
1f72399f44 Add Spanish translation
Spanish translation
2020-09-18 09:46:12 -04:00
KITblue
3aef62f201 Update Chinese translation
* Update zn.json

Already formatted

* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

* Update zn.json

* Add files via upload
2020-09-17 13:43:51 -04:00
dependabot-preview[bot]
e5535f6aff Bump @testing-library/user-event from 12.1.3 to 12.1.5 in /ui
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 12.1.3 to 12.1.5.
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/master/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v12.1.3...v12.1.5)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-16 20:20:57 -04:00
dependabot-preview[bot]
76fc5b1425 Bump @testing-library/react from 11.0.2 to 11.0.4 in /ui
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 11.0.2 to 11.0.4.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/master/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v11.0.2...v11.0.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-16 20:09:00 -04:00
dependabot-preview[bot]
a38f205c0b Bump react-measure from 2.5.0 to 2.5.2 in /ui
Bumps [react-measure](https://github.com/souporserious/react-measure) from 2.5.0 to 2.5.2.
- [Release notes](https://github.com/souporserious/react-measure/releases)
- [Changelog](https://github.com/souporserious/react-measure/blob/main/CHANGELOG.md)
- [Commits](https://github.com/souporserious/react-measure/compare/v2.5.0...v2.5.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-16 15:54:53 -04:00
Deluan
944107cb3d Update Italian translation (thanks @matteoipri) 2020-09-14 20:00:30 -04:00
Deluan
94fd0a10b5 Fix titles in Playlist create/edit views 2020-09-14 19:56:05 -04:00
Deluan
669f293f1f Fix ci-goreleaser 2020-09-10 17:49:25 -04:00
Deluan
532833ac7c Fix ci-goreleaser 2020-09-10 17:02:52 -04:00
Deluan
59f1d7e88a Use new ci-goreleaser, to fix generating Linux binaries for old kernels 2020-09-10 16:24:39 -04:00
Deluan
caeff2862a Remove dependency on C++17 2020-09-10 15:16:47 -04:00
Deluan
841c1129ff Break-up album/artist refresh in chunks 2020-09-09 08:57:59 -04:00
Deluan
ba30f7f8be Fix label for items per page (not always rows) 2020-09-08 14:55:41 -04:00
Deluan Quintão
6026638c03 Update fr.json (POEditor.com) 2020-09-08 14:54:02 -04:00
Deluan
cbab2e4eec go mod tidy 2020-09-08 13:33:07 -04:00
Deluan
a3ecc41e47 Change taglib extractor log level to trace 2020-09-08 13:33:07 -04:00
Deluan
4d18212f5d Extract all id3 frames from file 2020-09-08 13:33:07 -04:00
Deluan
5dea258058 Extract basic tags, as a fallback 2020-09-08 13:33:07 -04:00
Deluan
0802ab73d7 Trim tag value, not tag key 2020-09-08 13:33:07 -04:00
Deluan
865b9cd545 Trim spaces from tags 2020-09-08 13:33:07 -04:00
Deluan
e70ec53983 Rewrite taglib integration, now with TCMP 2020-09-08 13:33:07 -04:00
Deluan
2d0031f709 Parse more date formats 2020-09-08 13:33:07 -04:00
Deluan
78ecda5239 Get the first occurrence of multi-valued tags 2020-09-08 13:33:07 -04:00
Deluan
a1879ff871 Reorganize tests 2020-09-08 13:33:07 -04:00
Deluan
34eda3c8fc Add config option to select tag extractor (taglib, ffmpeg) 2020-09-08 13:33:07 -04:00
Deluan
506899b083 Add more fallback options for main tags 2020-09-08 13:33:07 -04:00
Deluan
3a4e2523dd Fix possible concurrency issue 2020-09-08 13:33:07 -04:00
Deluan
674b56a53d Install taglib in lint and go jobs 2020-09-08 13:33:07 -04:00
Deluan
58a0c44600 Embed audiotags lib, to make it static compilable 2020-09-08 13:33:07 -04:00
Deluan
df4328819d Initial implementation of taglib MetadataExtractor 2020-09-08 13:33:07 -04:00
Deluan
b6aa6eb7b2 Disable some jobs for now, as taglib is not available 2020-09-08 13:33:07 -04:00
Deluan
1187ee7cc1 Moved Metadata Extraction to its own package 2020-09-08 13:33:07 -04:00
Deluan
0beec552b1 Introduce Metadata and MetadataExtractor interfaces 2020-09-08 13:33:07 -04:00
Deluan
6a6d4c3f87 Use new ci-releaser image, that contains static taglib library 2020-09-08 13:33:07 -04:00
Deluan
1216c9bdb8 Bump react-measure version to 2.5.0 2020-09-08 13:12:00 -04:00
Deluan
2a888395fa Bump prettier version to 2.1.1 2020-09-08 13:06:34 -04:00
Deluan
56772f5c62 Bump @testing libraries 2020-09-08 13:05:04 -04:00
Deluan
07b5469b4c Bump uuid to v.1.1.2 2020-09-08 13:00:04 -04:00
Deluan
58324b411f Bump ginkgo to v1.14.1 2020-09-08 12:59:06 -04:00
dependabot-preview[bot]
c0e5b445cf Bump github.com/onsi/gomega from 1.10.1 to 1.10.2
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.10.1 to 1.10.2.
- [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.10.1...v1.10.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-08 12:55:59 -04:00
Deluan
6820e120cb Test for accented article sanitization 2020-09-08 09:40:41 -04:00
Deluan
28aefb4858 Fix sanitizing accented articles 2020-09-08 09:36:08 -04:00
Deluan
e50a720818 Sort by album name, then artist name 2020-09-07 16:21:29 -04:00
Deluan
900337081b Upgrade React-Player to 4.18.0 2020-09-06 12:11:02 -04:00
Deluan
34af6fc671 Clean up code a bit 2020-09-06 11:54:30 -04:00
Deluan
a25044bdf6 Reorder action buttons 2020-09-06 11:44:15 -04:00
Anders Moberg
30e98843ed Adding playlist button to Album Actions 2020-09-06 11:35:33 -04:00
Anders Moberg
8fe335ed97 Adding playlist button to Playlist actions 2020-09-06 11:35:33 -04:00
Deluan
8549451ee7 Fix potential undefined property
Not sure the reason, but I got this error:

```
Cannot read property 'id' of undefined
    at tn (SongTitleField.js:35)
    at Ka (react-dom.production.min.js:153)
    at vl (react-dom.production.min.js:261)
    at sc (react-dom.production.min.js:246)
    at lc (react-dom.production.min.js:246)
```
2020-09-02 12:41:21 -04:00
Deluan
596a4897a3 Do not force username to always be lowercase in the DB 2020-09-01 18:00:19 -04:00
Deluan
95eea0e9f8 Update ja.json (POEditor.com) (+2 squashed commits)
Squashed commits:
[e9c4218] Update ja.json (POEditor.com)
[10d3992] Add initial Japanese translation
2020-09-01 12:43:59 -04:00
Deluan Quintão
61c286a77e Update pl.json (POEditor.com) (+1 squashed commit)
Squashed commits:
[5c45ca0] Create pl.json
2020-09-01 12:43:59 -04:00
Deluan Quintão
15d11a9519 Update fr.json (POEditor.com) 2020-09-01 12:43:59 -04:00
Deluan Quintão
35625020e2 Update cs.json (POEditor.com) 2020-09-01 12:43:59 -04:00
Deluan
76e522710a New option: SearchFullString, to match query strings anywhere in searchable fields, not only in word boundaries
Based on feedback from @orlea, in https://github.com/deluan/navidrome/issues/255#issuecomment-683427754
2020-08-30 13:08:10 -04:00
Deluan Quintão
aae9d89e8c Update README.md 2020-08-26 13:54:23 -04:00
Deluan
0eae6d2a61 Hide "star" from disc subtitle rows 2020-08-25 22:48:05 -04:00
Deluan
f6982fd8ae Remove unused prop 2020-08-25 19:07:51 -04:00
Deluan
b364170d4f Remove duplicated star code from SongContextMenu 2020-08-24 19:51:41 -04:00
Deluan
0aceda9b89 Add star button to album detail view 2020-08-22 23:41:25 -04:00
Deluan
9df405a8ce Add export as m3u button to playlist 2020-08-22 13:23:50 -04:00
Deluan
366054e8cc Handle exporting playlists as m3u files 2020-08-22 12:15:26 -04:00
Deluan
8fa5544af7 Add option to download playlist 2020-08-21 13:28:20 -04:00
Deluan
073e40dc87 Add album cover lightbox 2020-08-21 12:41:23 -04:00
Deluan
a45c08f217 Ignore "hidden" files when importing a folder 2020-08-21 11:50:18 -04:00
Deluan
6c8535c54a Add support for reading webp artwork 2020-08-21 11:33:23 -04:00
Deluan
e2e79d6471 Fix getTopSongs endpoint mapping 2020-08-20 11:27:38 -04:00
Deluan
b5567090ed Remove -e option from grep, make the command more portable 2020-08-19 15:40:31 -04:00
Deluan
b836871161 Handle CR, LF and CRLF line endings when importing Playlists 2020-08-19 12:22:41 -04:00
Deluan
45e708f591 Loosen up constraints for email. Fixes #362 2020-08-19 12:22:41 -04:00
Deluan
608129963f Fix migration target 2020-08-19 11:18:30 -04:00
Deluan
f3d8222ddb Fix color of star in Album grid when using Light theme 2020-08-19 11:12:12 -04:00
Deluan
c83808a445 Revert "Use outlined Material-UI variant for login inputs as well"
This reverts commit c23e5c291c.
2020-08-18 09:58:09 -04:00
Deluan
c23e5c291c Use outlined Material-UI variant for login inputs as well 2020-08-17 16:10:49 -04:00
Deluan
bd1c3d9229 Use outlined Material-UI variant for all inputs 2020-08-17 11:19:39 -04:00
ericgaspar
48c0e1ca4b correct french translations 2020-08-17 09:22:20 -04:00
Deluan
16397e08fc Close cache reader. Should fix #446 2020-08-17 09:14:08 -04:00
Deluan
15a06fcd27 Removed support for Jamstash in dev mode. Not needed anymore :) 2020-08-15 23:11:31 -04:00
Deluan
a2e0acd6a2 Fix starring albums. Seems I may have lost a commit? 2020-08-15 15:03:03 -04:00
Deluan
5f38e70a2b Bump react-redux to 7.2.1 2020-08-15 12:58:22 -04:00
Deluan
c19c599521 Bump @testing-library 2020-08-15 12:57:18 -04:00
Deluan
dd398224e7 go mod tidy 2020-08-15 10:48:56 -04:00
Deluan
5ac76ae7e0 Fix broken image href 2020-08-14 17:00:24 -04:00
Deluan
c14147e6c5 More updated screenshots 2020-08-14 16:59:45 -04:00
Deluan
59ce940cd6 Use new screenshot in README 2020-08-14 16:53:36 -04:00
Deluan
cfecd7c6a2 Add new screenshot 2020-08-14 16:52:54 -04:00
Deluan
d81a4472a0 Update Czech translation 2020-08-14 16:32:30 -04:00
Deluan
147d26fb75 Enable sort by "starred" in Album and Artist lists 2020-08-14 15:35:15 -04:00
Deluan
848318932d Remove unused import 2020-08-14 14:47:54 -04:00
Deluan
49153dc1c1 Add playCount to artist list 2020-08-14 14:35:00 -04:00
Deluan
ca5da5b0ea Use active filters when shuffling songs 2020-08-14 14:10:39 -04:00
Deluan
c2e03c8162 Add stars to Albums 2020-08-14 13:35:28 -04:00
Deluan
f2ebbd26fa Add stars to Artist 2020-08-14 13:19:32 -04:00
Deluan
bbc4f9f91f Add artist context menu 2020-08-14 12:55:22 -04:00
Deluan
6fe1f84c68 Add download for songs 2020-08-14 12:11:35 -04:00
Deluan
d72468003f User album or artist name as zip name in download endpoint 2020-08-14 12:10:37 -04:00
Deluan
100f6a0645 Removed engine.Users 2020-08-14 12:10:37 -04:00
Deluan
bc2073fbd5 Removed unused function 2020-08-14 12:10:37 -04:00
Deluan
278d0ea8f3 Fix album fields in simulated browsing by folder 2020-08-14 12:10:37 -04:00
Deluan
0e16d7cfbb Fix regression: Show artwork in Music Stash when browsing by folder 2020-08-14 12:10:37 -04:00
Deluan
419884db7c Removed engine.Scrobbler 2020-08-14 12:10:37 -04:00
Deluan
eacfc41665 Removed engine.Search 2020-08-14 12:10:37 -04:00
Deluan
c271aa24d1 Make all Subsonic helper functions private 2020-08-14 12:10:37 -04:00
Deluan
22f34b3347 Refactor getGenres. Remove engine.Browser 2020-08-14 12:10:37 -04:00
Deluan
eba8395146 Refactor getSong 2020-08-14 12:10:37 -04:00
Deluan
f16dc5f8f8 Refactor getMusicDirectory 2020-08-14 12:10:37 -04:00
Deluan
15c8f4c0ef Refactor getAlbum 2020-08-14 12:10:37 -04:00
Deluan
e344f616b3 Refactor getArtist 2020-08-14 12:10:37 -04:00
Deluan
ef81caf3ed Refactor getMusicFolders and getIndexes 2020-08-14 12:10:37 -04:00
dependabot-preview[bot]
8513f1a899 Bump github.com/spf13/viper from 1.7.0 to 1.7.1
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.7.0 to 1.7.1.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.7.0...v1.7.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-08-14 09:44:35 -04:00
dependabot-preview[bot]
a9a25713e8 Bump github.com/microcosm-cc/bluemonday from 1.0.3 to 1.0.4
Bumps [github.com/microcosm-cc/bluemonday](https://github.com/microcosm-cc/bluemonday) from 1.0.3 to 1.0.4.
- [Release notes](https://github.com/microcosm-cc/bluemonday/releases)
- [Commits](https://github.com/microcosm-cc/bluemonday/compare/v1.0.3...v1.0.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-08-14 08:01:50 -04:00
Deluan
a5e1986072 Fix getTopSongs endpoint 2020-08-13 18:56:13 -04:00
Deluan Quintão
97c98e3369 Update tr.json (POEditor.com) 2020-08-13 17:19:25 -04:00
Deluan Quintão
6effd603e2 Update de.json (POEditor.com) 2020-08-13 17:19:25 -04:00
Deluan Quintão
8a783ef967 Update fr.json (POEditor.com) 2020-08-13 17:19:25 -04:00
Deluan
b74bd30b72 Fix Security Issue CVE-2020-7660 2020-08-13 11:14:13 -04:00
Deluan Quintão
9fa09e41cc Update README.md 2020-08-11 16:05:23 -04:00
Deluan
4ef12f91e0 Support Linux 32 bits releases 2020-08-07 13:36:00 -04:00
Deluan
0730c667a2 Add "Shuffle All" option to Song List. Closes #256 2020-08-07 10:47:55 -04:00
Deluan
4ec451aecb Add content-disposition header to set a download name 2020-08-05 18:40:46 -04:00
Deluan
883dd7f728 Use Outlined download icon
Also remove dangling console.log
2020-08-05 15:25:59 -04:00
Deluan
38c19eddc3 Add 'download' option to album context menu 2020-08-05 14:57:59 -04:00
Deluan
8e4b2e1c06 Add GetTopSongs placeholder, to make AVSub work 2020-08-05 13:48:50 -04:00
Deluan
a541afbfba Revert "Return absolute paths in Subsonic API responses"
This reverts commit 338cbacb
2020-08-05 12:37:43 -04:00
Deluan
df05760769 Move engine package under subsonic, as it should only be used by the Subsonic API.master
The idea is to move reusable code from `engine` to `core`, in future refactorings
2020-08-04 21:29:35 -04:00
Deluan
9a1133601a Store uncompressed files in zip 2020-08-04 13:38:32 -04:00
Deluan
2c370cae28 Support downloading full album and artist discography through Subsonic API 2020-08-04 12:39:13 -04:00
Deluan
f745b8d223 Use transaction's DataStore 2020-08-04 11:53:19 -04:00
Deluan
f1b6703ab0 Update React Player, fix song title maxWidth
See https://github.com/lijinke666/react-music-player/issues/141
2020-08-04 08:41:30 -04:00
Deluan
28d1428c90 Add option to disable .m3u auto-import 2020-08-02 23:17:13 -04:00
Deluan
696a0feb31 Remove ratings from engine package 2020-08-02 17:58:07 -04:00
Deluan
f29e1eb248 Remove repeated call 2020-08-02 15:19:42 -04:00
Deluan
d4e599233e Increase timeout of lint job in pipeline 2020-08-02 14:53:47 -04:00
Deluan
aaec8e080b Remove unused code 2020-08-02 11:16:46 -04:00
Deluan Quintão
09442eccd4 Update README.md 2020-08-01 23:29:27 -04:00
Deluan
21b9f51b71 Rename migrations package, to match goose generated migration files 2020-08-01 16:49:01 -04:00
Deluan
ed726c2126 Better implementation of Bookmarks, using its own table 2020-08-01 12:17:15 -04:00
Deluan
23d69d26e0 Add Bookmarks to Subsonic API 2020-07-31 17:45:49 -04:00
Deluan
3d0e70e907 Add MediaFile to Bookmark 2020-07-31 17:45:49 -04:00
Deluan
34e843a4b3 Add updatedAt to Bookmarks 2020-07-31 17:45:49 -04:00
Deluan
924ada0dab Add bookmark API repsonse 2020-07-31 17:45:49 -04:00
Deluan
2d3ed85311 Add bookmark in persistence layer 2020-07-31 17:45:49 -04:00
Deluan
3d4f4b4e2b Fix lint errors 2020-07-31 17:45:49 -04:00
Deluan
338cbacb79 Return absolute paths in Subsonic API responses 2020-07-31 17:45:49 -04:00
Deluan
0cf574198e Use Last.FM "white star" URL for artist info 2020-07-31 17:45:49 -04:00
Deluan
3000238a3c Implements the get/save play queue Subsonic endpoints and bumps API version to 1.12.0 2020-07-31 17:45:49 -04:00
Deluan
16c38eb344 Add PlayQueue Subsonic response 2020-07-31 17:45:49 -04:00
Deluan
721a959735 Create playqueue table and repository 2020-07-31 17:45:49 -04:00
Deluan
3c2b14d362 Rename make target for creating a new migration 2020-07-31 11:38:56 -04:00
Deluan
2b59d4b87a Rename 'Cover' to the more generic term 'Artwork' 2020-07-31 11:38:56 -04:00
Deluan
cefdeee495 Update Danish translations 2020-07-30 14:26:32 -04:00
Deluan
3383327c51 Show year range over the album art when in "artist view" mode 2020-07-29 22:34:33 -04:00
dependabot-preview[bot]
38b341ebc5 [Security] Bump elliptic from 6.5.2 to 6.5.3 in /ui
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3. **This update includes a security fix.**
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-29 18:47:01 -04:00
Deluan
ef0e5b130d Add a xl breakpoint to the album grid 2020-07-29 15:42:03 -04:00
Deluan
3092f83a00 Add option to select default album view 2020-07-29 15:34:48 -04:00
Deluan
8daac43e99 Add list type to album list view title 2020-07-29 15:34:48 -04:00
Deluan
d5da23ae42 Redirect from plain /album path to a default album list 2020-07-29 15:34:48 -04:00
Deluan
eae46d15bf Fix pagination 2020-07-29 15:34:48 -04:00
Deluan
f6c518fd8b Add Portuguese translation for album lists 2020-07-29 15:34:48 -04:00
Deluan
db8a48bba6 Implement album lists 2020-07-29 15:34:48 -04:00
Deluan
d877928f11 Add UpdatedAt to transcoding cache key 2020-07-28 17:16:01 -04:00
Deluan
0403ec2a07 Use OS-independent path separators 2020-07-28 08:49:07 -04:00
Deluan
8d27c77c2c Highlight compilations in Features 2020-07-27 15:00:03 -04:00
Deluan
f992b5663f Remove old scanner 2020-07-27 12:34:44 -04:00
Deluan
4e4fcb2304 Small refactorings, better var/function names 2020-07-27 10:51:50 -04:00
Deluan
ddb30ceb11 Add a v prefix to the version in the description 2020-07-26 10:52:20 -04:00
Deluan
67da83c84d Use a RWMutex instead of an AtomicBool, to reduce contention 2020-07-26 00:45:33 -04:00
Deluan
f8f16d676d Fix Cached flag 2020-07-24 18:48:28 -04:00
Deluan
58b816c2ed Show cached in info log 2020-07-24 18:43:03 -04:00
Deluan
9b1d5c196f Load cache asynchronously 2020-07-24 16:54:04 -04:00
Deluan
a0bed9beeb Handle missing index.html template 2020-07-24 13:59:41 -04:00
Deluan
9f4f2f7381 Use new FileCache in cover service 2020-07-24 13:30:27 -04:00
Deluan
433e31acc8 Refactor FileCache, allow disabling Trasncoding cache 2020-07-24 12:42:11 -04:00
Deluan
b795ad55a3 Allow SeekStart in a merged dir 2020-07-23 22:00:59 -04:00
Deluan
72efc18158 Allow translations to be overridden in the data folder 2020-07-23 18:11:10 -04:00
Deluan
93626129b6 Also import .m3u8 playlists 2020-07-23 03:26:39 -04:00
Deluan
60178c264d Keep annotations if tracks were already in DB 2020-07-23 03:26:39 -04:00
Deluan Quintão
de6afa16ec Update da.json 2020-07-22 16:07:22 -04:00
Deluan Quintão
fd2df12263 Update cs.json (POEditor.com) 2020-07-22 15:39:50 -04:00
Deluan
37d66a7d41 Add Danish translation 2020-07-22 15:39:50 -04:00
Deluan
040c7f1e7d Add missing call to refresh artists 2020-07-22 15:37:24 -04:00
Deluan
d4a5508f6a Remove LogLevel from Dockerfile 2020-07-22 12:56:50 -04:00
Deluan
036f9d6730 Flush albums and artists after each folder added/updated/deleted 2020-07-22 12:56:50 -04:00
Deluan
1b7f628759 Add tests for paths with UTF8 chars 2020-07-22 11:48:09 -04:00
Deluan
5a891fda9e Handle utf8 chars in paths 2020-07-22 09:36:22 -04:00
Deluan
f96e2f6c4f Process deleted folders even if there are no changed folders 2020-07-22 01:29:44 -04:00
Deluan
7a5285ae47 When deleting folders, only flush artists/albums after deleting the mediaFiles 2020-07-22 01:00:16 -04:00
Deluan
ba347bc0b1 Detect moved folders 2020-07-22 00:42:12 -04:00
Deluan
1bee98af52 Increase streamer test timeout 2020-07-21 20:43:59 -04:00
Deluan
ff623a8dce Run pre-push linting in verbose more 2020-07-21 20:30:04 -04:00
Deluan
f28e8118dc Strip 'v' prefix from version, to make it consistent for release and snapshot 2020-07-21 20:22:23 -04:00
Deluan
167fca86d0 Fix pipeline 2020-07-21 18:12:59 -04:00
Deluan
b828650cc5 Reduce the availability of old pipeline binaries artifacts 2020-07-21 18:11:09 -04:00
Deluan
e6846de0fa Small change, to trigger the pipeline that is stuck! 2020-07-21 17:46:35 -04:00
Deluan
6c6254a3c3 Get all git history when building the binaries 2020-07-21 17:37:36 -04:00
Deluan
0a9ad4e73a Bump action/upload-artifact and action/download-artifact to v2 2020-07-21 16:59:33 -04:00
Deluan
9f6eb4174f Do not upload packaged binaries as artifacts 2020-07-21 16:07:36 -04:00
Deluan
25cc523006 Output git tag info in the pipeline 2020-07-21 15:38:23 -04:00
Deluan
4c0000a809 Use Contributor Covenant v2.0 2020-07-21 14:40:21 -04:00
Deluan Quintão
0f7193f85d Create CODE_OF_CONDUCT.md 2020-07-21 14:19:34 -04:00
Deluan
715855280e Bump react-admin to 3.7.1 2020-07-21 13:15:03 -04:00
Deluan
c322253fde Upgrade react-player to 4.16.3 2020-07-21 13:06:33 -04:00
Deluan
17cea91e10 Bump @testing-library versions 2020-07-21 10:37:11 -04:00
Deluan
6caa5ee81f Bump react-ga from 3.0.0 to 3.1.2 2020-07-21 10:31:16 -04:00
Deluan
d46a8cf89f Allows config file to be specified with env var ND_CONFIGFILE. Fixes #415 2020-07-20 18:36:12 -04:00
Deluan
7e81a3b895 Fix default background image for login 2020-07-20 14:34:02 -04:00
680 changed files with 67881 additions and 17997 deletions

20
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.148.1/containers/go/.devcontainer/base.Dockerfile
# [Choice] Go version: 1, 1.15, 1.14
ARG VARIANT="1"
FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT}
# [Option] Install Node.js
ARG INSTALL_NODE="true"
ARG NODE_VERSION="lts/*"
RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
# [Optional] Uncomment this section to install additional OS packages.
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends libtag1-dev ffmpeg
# [Optional] Uncomment the next line to use go get to install anything else you need
# RUN go get -x <your-dependency-or-tool>
# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1

View File

@@ -0,0 +1,59 @@
{
"name": "Go",
"build": {
"dockerfile": "Dockerfile",
"args": {
// Update the VARIANT arg to pick a version of Go: 1, 1.15, 1.14
"VARIANT": "1.17",
// Options
"INSTALL_NODE": "true",
"NODE_VERSION": "v16"
}
},
"runArgs": [
"--cap-add=SYS_PTRACE",
"--security-opt",
"seccomp=unconfined"
],
// 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"
}
},
// 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,
4633
],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "make setup-dev",
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode",
"remoteEnv": {
"ND_MUSICFOLDER": "./music",
"ND_DATAFOLDER": "./data"
}
}

View File

@@ -1,6 +1,5 @@
.DS_Store
ui/node_modules
Jamstash-master
Dockerfile
docker-compose*.yml
data
@@ -9,5 +8,3 @@ testDB
navidrome
navidrome.db
navidrome.toml
assets/*gen.go

View File

@@ -1,2 +1,4 @@
# Upgrade Prettier to 2.0.4. Reformatted all JS files
b3f70538a9138bc279a451f4f358605097210d41
# Move project to Navidrome GitHub organization
6ee45a9ccc5e7ea4290c89030e67c99c0514bd25

2
.github/FUNDING.yml vendored
View File

@@ -3,8 +3,8 @@
github: deluan
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
liberapay: deluan
ko_fi: deluan
liberapay: deluan
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
issuehunt: # Replace with a single IssueHunt username

37
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

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

View File

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

12
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/ui"
schedule:
interval: weekly
open-pull-requests-limit: 10
- package-ecosystem: gomod
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

After

Width:  |  Height:  |  Size: 3.7 MiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 236 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 709 KiB

After

Width:  |  Height:  |  Size: 736 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 411 KiB

After

Width:  |  Height:  |  Size: 886 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -7,6 +7,7 @@ RUN echo "Target Platform = ${TARGETPLATFORM}"
COPY dist .
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then cp navidrome_linux_amd64_linux_amd64/navidrome /navidrome; fi
RUN if [ "$TARGETPLATFORM" = "linux/386" ]; then cp navidrome_linux_386_linux_386/navidrome /navidrome; fi
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then cp navidrome_linux_arm64_linux_arm64/navidrome /navidrome; fi
RUN if [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then cp navidrome_linux_arm_linux_arm_6/navidrome /navidrome; fi
RUN if [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then cp navidrome_linux_arm_linux_arm_7/navidrome /navidrome; fi
@@ -27,7 +28,6 @@ COPY --from=copy-binary /navidrome /app/
VOLUME ["/data", "/music"]
ENV ND_MUSICFOLDER /music
ENV ND_DATAFOLDER /data
ENV ND_LOGLEVEL info
ENV ND_PORT 4533
ENV GODEBUG "asyncpreemptoff=1"

View File

@@ -9,106 +9,140 @@ on:
branches:
- master
jobs:
golangci-lint:
name: Lint Server
go-lint:
name: Lint Go code
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v1
- name: Install taglib
run: sudo apt-get install libtag1-dev
- name: Set up Go 1.17
uses: actions/setup-go@v2
with:
version: v1.27
go-version: 1.17
id: go
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.40
github-token: ${{ secrets.GITHUB_TOKEN }}
args: --timeout 2m
- name: Install goimports
run: go install golang.org/x/tools/cmd/goimports
- run: goimports -w `find . -name '*.go' | grep -v '_gen.go$'`
- run: go mod tidy
- name: Verify no changes from goimports and go mod tidy
run: |
git status --porcelain
if [ -n "$(git status --porcelain)" ]; then
exit 1
fi
go:
name: Test Server on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
name: Test with Go ${{ matrix.go_version }}
runs-on: ubuntu-latest
strategy:
matrix:
# TODO Fix tests in Windows
# os: [macOS-latest, ubuntu-latest, windows-latest]
os: [macOS-latest, ubuntu-latest]
go_version: [1.16.x, 1.17.x]
steps:
- name: Set up Go 1.14
uses: actions/setup-go@v1
- name: Install taglib
run: sudo apt-get install libtag1-dev
- name: Set up Go ${{ matrix.go_version }}
uses: actions/setup-go@v2
with:
go-version: 1.14
stable: '!contains(${{ matrix.go_version }}, "beta") && !contains(${{ matrix.go_version }}, "rc")'
go-version: ${{ matrix.go_version }}
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- uses: actions/cache@v1
- uses: actions/cache@v2
id: cache-go
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
key: ${{ runner.os }}-go-${{ matrix.go_version }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
${{ runner.os }}-go-${{ matrix.go_version }}-
- name: Download dependencies
if: steps.cache-go.outputs.cache-hit != 'true'
continue-on-error: ${{contains(matrix.go_version, 'beta') || contains(matrix.go_version, 'rc')}}
run: go mod download
- name: Test
continue-on-error: ${{contains(matrix.go_version, 'beta') || contains(matrix.go_version, 'rc')}}
run: go test -cover ./... -v
js:
name: Build JS bundle
runs-on: ubuntu-latest
env:
NODE_OPTIONS: '--max_old_space_size=4096'
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: 14
- uses: actions/cache@v1
id: cache-npm
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('ui/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
node-version: 16
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: npm install dependencies
run: |
cd ui
npm ci
- name: npm check-formatting
- name: npm lint
run: |
cd ui
npm run check-formatting
npm run check-formatting && npm run lint
- name: npm test
run: |
cd ui
npm test
- name: npm build
run: |
cd ui
npm run build
- uses: actions/upload-artifact@v1
- uses: actions/upload-artifact@v2
with:
name: js-bundle
path: ui/build
retention-days: 7
binaries:
name: Binaries
needs: [js, go, golangci-lint]
name: Build binaries
needs: [js, go, go-lint]
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Unshallow
run: git fetch --prune --unshallow
- uses: actions/download-artifact@v1
- uses: actions/download-artifact@v2
with:
name: js-bundle
path: ui/build
- name: Show Tags
run: git tag
- name: Show Version
run: git describe --tags
- name: Run GoReleaser - SNAPSHOT
if: startsWith(github.ref, 'refs/tags/') != true
uses: docker://deluan/ci-goreleaser:1.14.4-2
uses: docker://deluan/ci-goreleaser:1.17.1-1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -116,36 +150,42 @@ jobs:
- name: Run GoReleaser - RELEASE
if: startsWith(github.ref, 'refs/tags/')
uses: docker://deluan/ci-goreleaser:1.14.4-2
uses: docker://deluan/ci-goreleaser:1.17.1-1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: goreleaser release --rm-dist
- uses: actions/upload-artifact@v1
- uses: actions/upload-artifact@v2
with:
name: binaries
path: dist
path: |
dist
!dist/*.tar.gz
!dist/*.zip
retention-days: 7
docker:
name: Docker images
name: Build Docker images
needs: [binaries]
runs-on: ubuntu-latest
env:
DOCKER_IMAGE: ${{secrets.DOCKER_IMAGE}}
steps:
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
if: env.DOCKER_IMAGE != ''
- name: Set up Docker Buildx
id: buildx
uses: crazy-max/ghaction-docker-buildx@v1
if: env.DOCKER_IMAGE != ''
with:
buildx-version: latest
qemu-version: latest
- uses: actions/checkout@v1
uses: docker/setup-buildx-action@v1
if: env.DOCKER_IMAGE != ''
- uses: actions/download-artifact@v1
- uses: actions/checkout@v2
if: env.DOCKER_IMAGE != ''
- uses: actions/download-artifact@v2
if: env.DOCKER_IMAGE != ''
with:
name: binaries
@@ -155,7 +195,7 @@ jobs:
if: env.DOCKER_IMAGE != ''
env:
DOCKER_IMAGE: ${{secrets.DOCKER_IMAGE}}
DOCKER_PLATFORM: linux/amd64,linux/arm/v7,linux/arm64
DOCKER_PLATFORM: linux/amd64,linux/386,linux/arm/v6,linux/arm/v7,linux/arm64
run: |
echo ${{secrets.DOCKER_PASSWORD}} | docker login -u ${{secrets.DOCKER_USERNAME}} --password-stdin
docker buildx build --platform ${DOCKER_PLATFORM} `.github/workflows/docker-tags.sh` -f .github/workflows/pipeline.dockerfile --push .

View File

@@ -1,18 +0,0 @@
name: Remove old artifacts
on:
schedule:
# Every day at 1am
- cron: '0 1 * * *'
jobs:
remove-old-artifacts:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Remove old artifacts
uses: c-hive/gha-remove-artifacts@v1
with:
age: '7 days'
skip-tags: false

3
.gitignore vendored
View File

@@ -12,7 +12,6 @@ TODO.md
var
navidrome.toml
master.zip
Jamstash-master
testDB
navidrome.db
*.swp
@@ -23,4 +22,4 @@ docker-compose.yml
navidrome.db-shm
navidrome.db-wal
tags
.gitinfo

View File

@@ -2,10 +2,10 @@ linters:
enable:
- bodyclose
- deadcode
- depguard
- dogsled
- errcheck
- gocyclo
- goimports
- goprintffuncname
- gosec
- gosimple
@@ -26,4 +26,4 @@ issues:
exclude-rules:
- linters:
- gosec
text: "(G501|G401):"
text: "(G501|G401|G505):"

View File

@@ -1,26 +1,7 @@
# GoReleaser config
project_name: navidrome
before:
hooks:
- go-bindata -fs -prefix resources -tags embed -ignore="\\\*.go" -pkg resources -o resources/embedded_gen.go resources/...
- go-bindata -fs -prefix ui/build -tags embed -nocompress -pkg assets -o assets/embedded_gen.go ui/build/...
builds:
- id: navidrome_darwin
env:
- CGO_ENABLED=1
- CC=o64-clang
- CXX=o64-clang++
goos:
- darwin
goarch:
- amd64
flags:
- -tags=embed
ldflags:
- -s -w -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
- id: navidrome_linux_amd64
env:
- CGO_ENABLED=1
@@ -29,15 +10,29 @@ builds:
goarch:
- amd64
flags:
- -tags=embed
- -tags=embed,netgo
ldflags:
- "-extldflags '-static -lz'"
- -s -w -X github.com/navidrome/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/navidrome/navidrome/consts.gitTag={{.Version}}
- id: navidrome_linux_386
env:
- CGO_ENABLED=1
goos:
- linux
goarch:
- 386
flags:
- -tags=embed,netgo
ldflags:
- "-extldflags '-static'"
- -s -w -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
- -s -w -X github.com/navidrome/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/navidrome/navidrome/consts.gitTag={{.Version}}
- id: navidrome_linux_arm
env:
- CGO_ENABLED=1
- CC=arm-linux-gnueabi-gcc-5
- CC=arm-linux-gnueabi-gcc
- CXX=arm-linux-gnueabi-g++
goos:
- linux
goarch:
@@ -47,54 +42,72 @@ builds:
- 6
- 7
flags:
- -tags=embed
- -tags=embed,netgo
ldflags:
- "-extldflags '-static'"
- -s -w -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
- -s -w -X github.com/navidrome/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/navidrome/navidrome/consts.gitTag={{.Version}}
- id: navidrome_linux_arm64
env:
- CGO_ENABLED=1
- CC=aarch64-linux-gnu-gcc-5
- CC=aarch64-linux-gnu-gcc
- CXX=aarch64-linux-gnu-g++
goos:
- linux
goarch:
- arm64
flags:
- -tags=embed
- -tags=embed,netgo
ldflags:
- "-extldflags '-static'"
- -s -w -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
- -s -w -X github.com/navidrome/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/navidrome/navidrome/consts.gitTag={{.Version}}
- id: navidrome_windows_i686
- id: navidrome_windows_386
env:
- CGO_ENABLED=1
- CC=i686-w64-mingw32-gcc
- CXX=i686-w64-mingw32-g++
- PKG_CONFIG_PATH=/mingw32/lib/pkgconfig
goos:
- windows
goarch:
- 386
flags:
- -tags=embed
- -tags=embed,netgo
ldflags:
- "-extldflags '-static'"
- -s -w -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
- -s -w -X github.com/navidrome/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/navidrome/navidrome/consts.gitTag={{.Version}}
- id: navidrome_windows_x64
- id: navidrome_windows_amd64
env:
- CGO_ENABLED=1
- CC=x86_64-w64-mingw32-gcc
- CXX=x86_64-w64-mingw32-g++
- PKG_CONFIG_PATH=/mingw64/lib/pkgconfig
goos:
- windows
goarch:
- amd64
flags:
- -tags=embed
- -tags=embed,netgo
ldflags:
- "-extldflags '-static'"
- -s -w -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
- -s -w -X github.com/navidrome/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/navidrome/navidrome/consts.gitTag={{.Version}}
- id: navidrome_darwin_amd64
env:
- CGO_ENABLED=1
- CC=o64-clang
- CXX=o64-clang++
- PKG_CONFIG_PATH=/darwin/lib/pkgconfig
goos:
- darwin
goarch:
- amd64
flags:
- -tags=embed,netgo
ldflags:
- -s -w -X github.com/navidrome/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/navidrome/navidrome/consts.gitTag={{.Version}}
archives:
- format_overrides:

2
.nvmrc
View File

@@ -1 +1 @@
v14
v16

129
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,129 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
navidrome@navidrome.org.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

92
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,92 @@
# Navidrome Contribution Guide
Navidrome is a streaming service which allows you to enjoy your music collection from anywhere. We'd welcome you to contribute to our open source project and make Navidrome even better. There are some basic guidelines which you need to follow if you like to contribute to Navidrome.
- [Code of Conduct](#code-of-conduct)
- [Issues](#issues)
- [Questions](#questions)
- [Pull Requests](#pull-requests)
## Code of Conduct
Please read the following [Code of Conduct](https://github.com/navidrome/navidrome/blob/master/CODE_OF_CONDUCT.md).
## Issues
Found any issue or bug in our codebase? Have a great idea you want to propose or discuss with
the developers? You can help by submitting an [issue](https://github.com/navidrome/navidrome/issues/new/choose)
to the Github repository.
**Before opening a new issue, please check if the issue has not been already made by searching
the [issues](https://github.com/navidrome/navidrome/issues)**
## Questions
We would like to have discussions and general queries related to Navidrome on our [Discord channel](https://discord.gg/2qMuMyHfSV).
## Pull requests
Before submitting a pull request, ensure that you go through the following:
- Open a corresponding issue for the Pull Request, if not existing. The issue can be opened following [these guidelines](#issues)
- Ensure that there is no open or closed Pull Request corresponding to your submission to avoid duplication of effort.
- Setup the [development environment](https://www.navidrome.org/docs/developers/dev-environment/)
- Create a new branch on your forked repo and make the changes in it. Naming conventions for branch are: `<Issue Title>/<Issue Number>`. Example:
```
git checkout -b adding-docs/834 master
```
- The commits should follow a [specific convention](#commit-conventions)
- Ensure that a DCO sign-off for commits is provided via `--signoff` option of git commit
- Provide a link to the issue that will be closed via your Pull request.
### Commit Conventions
Each commit message must adhere to the following format:
```
<type>(scope): <description> - <issue number>
[optional body]
```
This improves the readability of the messages
#### Type
It can be one of the following:
1. **feat**: Addition of a new feature
2. **fix**: Bug fix
3. **docs**: Documentation Changes
4. **style**: Changes to styling
5. **refactor**: Refactoring of code
6. **perf**: Code that affects performance
7. **test**: Updating or improving the current tests
8. **build**: Changes to Build process
9. **revert**: Reverting to a previous commit
10. **chore** : updating grunt tasks etc
If there is a breaking change in your Pull Request, please add `BREAKING CHANGE` in the optional body section
#### Scope
The file or folder where the changes are made. If there are more than one, you can mention any
#### Description
A short description of the issue
#### Issue number
The issue fixed by this Pull Request.
The body is optional. It may contain short description of changes made.
Following all the guidelines an ideal commit will look like:
```
git commit --signoff -m "feat(themes): New-theme - #834"
```
After committing, push your commits to your forked branch and create a Pull Request from there.
The Pull Request Title can be the same as `<type>(scope): <description> - <issue number>`
A demo layout of how the Pull request body can look:
```
Closes <Issue number along with link>
Description (What does the pull request do)
Changes (What changes were made )
Screenshots or Videos
Related Issues and Pull Requests(if any)
```

201
Makefile
View File

@@ -1,107 +1,152 @@
GO_VERSION=$(shell grep -e "^go " go.mod | cut -f 2 -d ' ')
GO_VERSION=$(shell grep "^go " go.mod | cut -f 2 -d ' ')
NODE_VERSION=$(shell cat .nvmrc)
ifneq ("$(wildcard .git)","")
GIT_SHA=$(shell git rev-parse --short HEAD)
GIT_TAG=$(shell git describe --tags `git rev-list --tags --max-count=1`)
else
GIT_SHA=source_archive
GIT_TAG=$(patsubst navidrome-%,v%,$(notdir $(PWD)))
endif
## Default target just build the Go project.
default:
go build -ldflags="-X github.com/deluan/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/deluan/navidrome/consts.gitTag=master"
.PHONY: default
CI_RELEASER_VERSION=1.17.1-1 ## https://github.com/navidrome/ci-goreleaser
dev: check_env
npx foreman -j Procfile.dev -p 4533 start
.PHONY: dev
server: check_go_env
@reflex -d none -c reflex.conf
.PHONY: server
wire: check_go_env
wire ./...
.PHONY: wire
watch: check_go_env
ginkgo watch -notify ./...
.PHONY: watch
test: check_go_env
go test ./... -v
.PHONY: test
testall: check_go_env test
@(cd ./ui && npm test -- --watchAll=false)
.PHONY: testall
update-snapshots: check_go_env
UPDATE_SNAPSHOTS=true ginkgo ./server/subsonic/...
.PHONY: update-snapshots
create-migration:
@if [ -z "${name}" ]; then echo "Usage: make create-migration name=name_of_migration_file"; exit 1; fi
goose -dir db/migration create ${name}
.PHONY: create-migration
setup:
@which go-bindata || (echo "Installing BinData" && GO111MODULE=off go get -u github.com/go-bindata/go-bindata/...)
go mod download
setup: check_env download-deps setup-git ##@1_Run_First Install dependencies and prepare development environment
@echo Downloading Node dependencies...
@(cd ./ui && npm ci)
.PHONY: setup
setup-dev: setup setup-git
@which wire || (echo "Installing Wire" && GO111MODULE=off go get -u github.com/google/wire/cmd/wire)
@which ginkgo || (echo "Installing Ginkgo" && GO111MODULE=off go get -u github.com/onsi/ginkgo/ginkgo)
@which goose || (echo "Installing Goose" && GO111MODULE=off go get -u github.com/pressly/goose/cmd/goose)
@which reflex || (echo "Installing Reflex" && GO111MODULE=off go get -u github.com/cespare/reflex)
@which golangci-lint || (echo "Installing GolangCI-Lint" && cd .. && GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.27.0)
@which goimports || (echo "Installing goimports" && GO111MODULE=off go get -u golang.org/x/tools/cmd/goimports)
dev: check_env ##@Development Start Navidrome in development mode, with hot-reload for both frontend and backend
npx foreman -j Procfile.dev -p 4533 start
.PHONY: dev
server: check_go_env ##@Development Start the backend in development mode
@go run github.com/cespare/reflex -d none -c reflex.conf
.PHONY: server
watch: ##@Development Start Go tests in watch mode (re-run when code changes)
go run github.com/onsi/ginkgo/ginkgo watch -notify ./...
.PHONY: watch
test: ##@Development Run Go tests
go test ./...
.PHONY: test
testall: test ##@Development Run Go and JS tests
@(cd ./ui && npm test -- --watchAll=false)
.PHONY: testall
lint: ##@Development Lint Go code
go run github.com/golangci/golangci-lint/cmd/golangci-lint run -v --timeout 5m
.PHONY: lint
lintall: lint ##@Development Lint Go and JS code
@(cd ./ui && npm run check-formatting && npm run lint)
.PHONY: lintall
wire: check_go_env ##@Development Update Dependency Injection
go run github.com/google/wire/cmd/wire ./...
.PHONY: wire
snapshots: ##@Development Update (GoLang) Snapshot tests
UPDATE_SNAPSHOTS=true go run github.com/onsi/ginkgo/ginkgo ./server/subsonic/...
.PHONY: snapshots
migration: ##@Development Create an empty migration file
@if [ -z "${name}" ]; then echo "Usage: make migration name=name_of_migration_file"; exit 1; fi
go run github.com/pressly/goose/cmd/goose -dir db/migration create ${name}
.PHONY: migration
setup-dev: setup
.PHONY: setup-dev
setup-git:
setup-git: ##@Development Setup Git hooks (pre-commit and pre-push)
@echo Setting up git hooks
@mkdir -p .git/hooks
(cd .git/hooks && ln -sf ../../git/* .)
@(cd .git/hooks && ln -sf ../../git/* .)
.PHONY: setup-git
Jamstash-master:
wget -N https://github.com/tsquillario/Jamstash/archive/master.zip
unzip -o master.zip
rm master.zip
(cd Jamstash-master && npm ci && npx bower install && npx grunt build)
rm -rf Jamstash-master/node_modules Jamstash-master/bower_components
buildall: buildjs build ##@Build Build the project, both frontend and backend
.PHONY: buildall
build: check_go_env ##@Build Build only backend
@echo "WARNING: This command does not build the frontend, it uses the latest one built by 'make buildjs'"
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
buildjs: check_node_env ##@Build Build only frontend
@(cd ./ui && npm run build)
.PHONY: buildjs
all: ##@Cross_Compilation Build binaries for all supported platforms. It does not build the frontend
@echo "WARNING: This command does not builds the frontend, it uses the latest one built by 'make buildjs'"
docker run -t -v $(PWD):/workspace -w /workspace deluan/ci-goreleaser:$(CI_RELEASER_VERSION) \
goreleaser release --rm-dist --skip-publish --snapshot
.PHONY: all
single: ##@Cross_Compilation Build binaries for a single supported platforms. It does not build the frontend
@echo "WARNING: This command does not build the frontend, it uses the latest one built by 'make buildjs'"
@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}"
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}
.PHONY: single
##########################################
#### Miscellaneous
release:
@if [[ ! "${V}" =~ ^[0-9]+\.[0-9]+\.[0-9]+.*$$ ]]; then echo "Usage: make release V=X.X.X"; exit 1; fi
go mod tidy
@if [ -n "`git status -s`" ]; then echo "\n\nThere are pending changes. Please commit or stash first"; exit 1; fi
make pre-push
git tag v${V}
git push origin v${V} --no-verify
.PHONY: release
download-deps:
@echo Downloading Go dependencies...
@go mod download -x
@go mod tidy # To revert any changes made by the `go mod download` command
.PHONY: download-deps
check_env: check_go_env check_node_env
.PHONY: check_env
check_go_env:
@(hash go) || (echo "\nERROR: GO environment not setup properly!\n"; exit 1)
@go version | grep -q $(GO_VERSION) || (echo "\nERROR: Please upgrade your GO version\nThis project requires version $(GO_VERSION)"; exit 1)
@current_go_version=`go version | cut -d ' ' -f 3 | cut -c3-` && \
echo "$(GO_VERSION) $$current_go_version" | \
tr ' ' '\n' | sort -V | tail -1 | \
grep -q "^$${current_go_version}$$" || \
(echo "\nERROR: Please upgrade your GO version\nThis project requires at least the version $(GO_VERSION)"; exit 1)
.PHONY: check_go_env
check_node_env:
@(hash node) || (echo "\nERROR: Node environment not setup properly!\n"; exit 1)
@node --version | grep -q $(NODE_VERSION) || (echo "\nERROR: Please check your Node version. Should be $(NODE_VERSION)\n"; exit 1)
@current_node_version=`node --version` && \
echo "$(NODE_VERSION) $$current_node_version" | \
tr ' ' '\n' | sort -V | tail -1 | \
grep -q "^$${current_node_version}$$" || \
(echo "\nERROR: Please check your Node version. Should be at least $(NODE_VERSION)\n"; exit 1)
.PHONY: check_node_env
build: check_go_env
go build -ldflags="-X github.com/deluan/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/deluan/navidrome/consts.gitTag=$(GIT_TAG)-SNAPSHOT"
.PHONY: build
pre-push: lintall testall
.PHONY: pre-push
buildall: check_env
@(cd ./ui && npm run build)
go-bindata -fs -prefix "resources" -tags embed -ignore="\\\*.go" -pkg resources -o resources/embedded_gen.go resources/...
go-bindata -fs -prefix "ui/build" -tags embed -nocompress -pkg assets -o assets/embedded_gen.go ui/build/...
go build -ldflags="-X github.com/deluan/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/deluan/navidrome/consts.gitTag=$(GIT_TAG)-SNAPSHOT" -tags=embed
.PHONY: buildall
.DEFAULT_GOAL := help
release:
@if [[ ! "${V}" =~ ^[0-9]+\.[0-9]+\.[0-9]+.*$$ ]]; then echo "Usage: make release V=X.X.X"; exit 1; fi
go mod tidy
@if [ -n "`git status -s`" ]; then echo "\n\nThere are pending changes. Please commit or stash first"; exit 1; fi
make test
git tag v${V}
git push origin v${V}
.PHONY: release
HELP_FUN = \
%help; while(<>){push@{$$help{$$2//'options'}},[$$1,$$3] \
if/^([\w-_]+)\s*:.*\#\#(?:@(\w+))?\s(.*)$$/}; \
print"$$_:\n", map" $$_->[0]".(" "x(20-length($$_->[0])))."$$_->[1]\n",\
@{$$help{$$_}},"\n" for sort keys %help; \
snapshot:
docker run -it -v $(PWD):/workspace -w /workspace deluan/ci-goreleaser:1.14.4-2 goreleaser release --rm-dist --skip-publish --snapshot
.PHONY: snapshot
help: ##@Miscellaneous Show this help
@echo "Usage: make [target] ...\n"
@perl -e '$(HELP_FUN)' $(MAKEFILE_LIST)

View File

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

View File

@@ -1,27 +1,28 @@
# Navidrome Music Streamer
<img src="resources/logo-192x192.png" alt="Navidrome logo" title="navidrome" align="right" height="60px" />
[![Last Release](https://img.shields.io/github/v/release/deluan/navidrome?label=latest&style=flat-square)](https://github.com/deluan/navidrome/releases)
[![Build](https://img.shields.io/github/workflow/status/deluan/navidrome/Build?style=flat-square)](https://github.com/deluan/navidrome/actions)
[![Downloads](https://img.shields.io/github/downloads/deluan/navidrome/total)](https://github.com/deluan/navidrome/releases/latest)
[![Docker Pulls](https://img.shields.io/docker/pulls/deluan/navidrome?style=flat-square)](https://hub.docker.com/r/deluan/navidrome)
[![Dev Chat](https://img.shields.io/discord/671335427726114836?label=chat&style=flat-square)](https://discord.gg/xh7j7yF)
[![Subreddit](https://img.shields.io/reddit/subreddit-subscribers/navidrome?style=flat-square)](https://www.reddit.com/r/navidrome/)
# Navidrome Music Server
[![Last Release](https://img.shields.io/github/v/release/navidrome/navidrome?logo=github&label=latest&style=flat-square)](https://github.com/navidrome/navidrome/releases)
[![Build](https://img.shields.io/github/workflow/status/navidrome/navidrome/Build?logo=github&style=flat-square)](https://nightly.link/navidrome/navidrome/workflows/pipeline/master)
[![Downloads](https://img.shields.io/github/downloads/navidrome/navidrome/total?logo=github&style=flat-square)](https://github.com/navidrome/navidrome/releases/latest)
[![Docker Pulls](https://img.shields.io/docker/pulls/deluan/navidrome?logo=docker&label=pulls&style=flat-square)](https://hub.docker.com/r/deluan/navidrome)
[![Dev Chat](https://img.shields.io/discord/671335427726114836?logo=discord&label=discord&style=flat-square)](https://discord.gg/xh7j7yF)
[![Subreddit](https://img.shields.io/reddit/subreddit-subscribers/navidrome?logo=reddit&label=/r/navidrome&style=flat-square)](https://www.reddit.com/r/navidrome/)
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0-ff69b4.svg?style=flat-square)](CODE_OF_CONDUCT.md)
## [Check out our Live Demo!](https://www.navidrome.org/demo/)
Navidrome is an open source web-based music collection server and streamer. It gives you freedom to listen to your
music collection from any browser or mobile device. It's like your personal Spotify!
__Any feedback is welcome!__ If you need/want a new feature, find a bug or think of any way to improve Navidrome,
please fill a [GitHub issue](https://github.com/deluan/navidrome/issues) or join the discussion in our
please file a [GitHub issue](https://github.com/navidrome/navidrome/issues) or join the discussion in our
[Subreddit](https://www.reddit.com/r/navidrome/). If you want to contribute to the project in any other way
([ui/backend dev](https://www.navidrome.org/docs/developers/),
[translations](https://www.navidrome.org/docs/developers/translations/),
[themes](https://www.navidrome.org/docs/developers/creating-themes)), please join the chat in our
[Discord server](https://discord.gg/xh7j7yF).
## Demo Site
To see Navidrome in action, check out our [live demo](https://www.navidrome.org/demo/)
## Installation
See instructions in the [project's website](https://www.navidrome.org/docs/installation/)
@@ -31,11 +32,11 @@ See instructions in the [project's website](https://www.navidrome.org/docs/insta
- Handles very **large music collections**
- Streams virtually **any audio format** available
- Reads and uses all your beautifully curated **metadata**
- Great support for **Box Sets** (multi-disc albums)
- Great support for **compilations** (Various Artists albums) and **box sets** (multi-disc albums)
- **Multi-user**, each user has their own play counts, playlists, favourites, etc...
- Very **low resource usage**
- **Multi-platform**, runs on macOS, Linux and Windows. **Docker** images are also provided
- Ready to use **Raspberry Pi** binaries and Docker images available
- Ready to use binaries for all major platforms, including **Raspberry Pi**
- Automatically **monitors your library** for changes, importing new files and reloading new metadata
- **Themeable**, modern and responsive **Web interface** based on [Material UI](https://material-ui.com)
- **Compatible** with all Subsonic/Madsonic/Airsonic [clients](https://www.navidrome.org/docs/overview/#apps)
@@ -57,8 +58,8 @@ Here are some useful direct links:
## Screenshots
<p align="left">
<img height="550" src="https://raw.githubusercontent.com/deluan/navidrome/master/.github/screenshots/ss-mobile-login.png">
<img height="550" src="https://raw.githubusercontent.com/deluan/navidrome/master/.github/screenshots/ss-mobile-player.png">
<img height="550" src="https://raw.githubusercontent.com/deluan/navidrome/master/.github/screenshots/ss-mobile-album-view.png">
<img width="550" src="https://raw.githubusercontent.com/deluan/navidrome/master/.github/screenshots/ss-desktop-player.png">
<img height="550" src="https://raw.githubusercontent.com/navidrome/navidrome/master/.github/screenshots/ss-mobile-login.png">
<img height="550" src="https://raw.githubusercontent.com/navidrome/navidrome/master/.github/screenshots/ss-mobile-player.png">
<img height="550" src="https://raw.githubusercontent.com/navidrome/navidrome/master/.github/screenshots/ss-mobile-album-view.png">
<img width="550" src="https://raw.githubusercontent.com/navidrome/navidrome/master/.github/screenshots/ss-desktop-player.png">
</p>

View File

@@ -1,19 +0,0 @@
// +build !embed
package assets
import (
"net/http"
"sync"
"github.com/deluan/navidrome/log"
)
var once sync.Once
func AssetFile() http.FileSystem {
once.Do(func() {
log.Warn("Using external assets from 'ui/build' folder")
})
return http.Dir("ui/build")
}

View File

@@ -1,12 +1,17 @@
package cmd
import (
"context"
"fmt"
"os"
"time"
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/db"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/db"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/scheduler"
"github.com/oklog/run"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -24,7 +29,7 @@ Complete documentation is available at https://www.navidrome.org/docs`,
preRun()
},
Run: func(cmd *cobra.Command, args []string) {
startServer()
runNavidrome()
},
Version: consts.Version(),
}
@@ -45,20 +50,116 @@ func preRun() {
conf.Load()
}
func startServer() {
func runNavidrome() {
db.EnsureLatestVersion()
subsonic, err := CreateSubsonicAPIRouter()
if err != nil {
panic(fmt.Sprintf("Could not create the Subsonic API router. Aborting! err=%v", err))
var g run.Group
g.Add(startServer())
g.Add(startSignaler())
g.Add(startScheduler())
schedule := conf.Server.ScanSchedule
if schedule != "" {
go schedulePeriodicScan(schedule)
} else {
log.Warn("Periodic scan is DISABLED")
}
if err := g.Run(); err != nil {
log.Error("Fatal error in Navidrome. Aborting", err)
}
a := CreateServer(conf.Server.MusicFolder)
a.MountRouter(consts.URLPathSubsonicAPI, subsonic)
a.MountRouter(consts.URLPathUI, CreateAppRouter())
a.Run(fmt.Sprintf("%s:%d", conf.Server.Address, conf.Server.Port))
}
// TODO: Implemement some struct tags to map flags to viper
func startServer() (func() error, func(err error)) {
return func() error {
a := CreateServer(conf.Server.MusicFolder)
a.MountRouter("Native API", consts.URLPathNativeAPI, CreateNativeAPIRouter())
a.MountRouter("Subsonic API", consts.URLPathSubsonicAPI, CreateSubsonicAPIRouter())
if conf.Server.LastFM.Enabled {
a.MountRouter("LastFM Auth", consts.URLPathNativeAPI+"/lastfm", CreateLastFMRouter())
}
return a.Run(fmt.Sprintf("%s:%d", conf.Server.Address, conf.Server.Port))
}, func(err error) {
if err != nil {
log.Error("Shutting down Server due to error", err)
} else {
log.Info("Shutting down Server")
}
}
}
var sigChan = make(chan os.Signal, 1)
func startSignaler() (func() error, func(err error)) {
scanner := GetScanner()
ctx, cancel := context.WithCancel(context.Background())
return func() error {
for {
select {
case sig := <-sigChan:
log.Info(ctx, "Received signal, triggering a new scan", "signal", sig)
start := time.Now()
err := scanner.RescanAll(ctx, false)
if err != nil {
log.Error(ctx, "Error scanning", err)
}
log.Info(ctx, "Triggered scan complete", "elapsed", time.Since(start).Round(100*time.Millisecond))
case <-ctx.Done():
break
}
}
}, func(err error) {
cancel()
if err != nil {
log.Error("Shutting down Signaler due to error", err)
} else {
log.Info("Shutting down Signaler")
}
}
}
func schedulePeriodicScan(schedule string) {
scanner := GetScanner()
schedulerInstance := scheduler.GetInstance()
log.Info("Scheduling periodic scan", "schedule", schedule)
err := schedulerInstance.Add(schedule, func() {
_ = scanner.RescanAll(context.Background(), false)
})
if err != nil {
log.Error("Error scheduling periodic scan", err)
}
time.Sleep(2 * time.Second) // Wait 2 seconds before the initial scan
log.Debug("Executing initial scan")
if err := scanner.RescanAll(context.Background(), false); err != nil {
log.Error("Error executing initial scan", err)
}
log.Debug("Finished initial scan")
}
func startScheduler() (func() error, func(err error)) {
log.Info("Starting scheduler")
schedulerInstance := scheduler.GetInstance()
ctx, cancel := context.WithCancel(context.Background())
return func() error {
schedulerInstance.Run(ctx)
return nil
}, func(err error) {
cancel()
if err != nil {
log.Error("Shutting down Scheduler due to error", err)
} else {
log.Info("Shutting down Scheduler")
}
}
}
// TODO: Implement some struct tags to map flags to viper
func init() {
cobra.OnInitialize(func() {
conf.InitConfig(cfgFile)
@@ -83,6 +184,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().Bool("autoimportplaylists", viper.GetBool("autoimportplaylists"), "enable/disable .m3u playlist auto-import`")
_ = viper.BindPFlag("address", rootCmd.Flags().Lookup("address"))
_ = viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))

View File

@@ -1,9 +1,10 @@
package cmd
import (
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/log"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
var fullRescan bool
@@ -23,11 +24,10 @@ var scanCmd = &cobra.Command{
}
func runScanner() {
scanner := CreateScanner(conf.Server.MusicFolder)
err := scanner.RescanAll(fullRescan)
if err != nil {
log.Error("Error scanning media folder", "folder", conf.Server.MusicFolder, err)
}
conf.Server.DevPreCacheAlbumArtwork = false
scanner := GetScanner()
_ = scanner.RescanAll(context.Background(), fullRescan)
if fullRescan {
log.Info("Finished full rescan")
} else {

17
cmd/signaler_unix.go Normal file
View File

@@ -0,0 +1,17 @@
//go:build !windows && !plan9
// +build !windows,!plan9
package cmd
import (
"os"
"os/signal"
"syscall"
)
func init() {
signals := []os.Signal{
syscall.SIGUSR1,
}
signal.Notify(sigChan, signals...)
}

View File

@@ -1,69 +1,95 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//go:generate go run github.com/google/wire/cmd/wire
//+build !wireinject
package cmd
import (
"github.com/deluan/navidrome/core"
"github.com/deluan/navidrome/core/transcoder"
"github.com/deluan/navidrome/engine"
"github.com/deluan/navidrome/persistence"
"github.com/deluan/navidrome/scanner"
"github.com/deluan/navidrome/server"
"github.com/deluan/navidrome/server/app"
"github.com/deluan/navidrome/server/subsonic"
"github.com/google/wire"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/agents"
"github.com/navidrome/navidrome/core/agents/lastfm"
"github.com/navidrome/navidrome/core/scrobbler"
"github.com/navidrome/navidrome/core/transcoder"
"github.com/navidrome/navidrome/db"
"github.com/navidrome/navidrome/persistence"
"github.com/navidrome/navidrome/scanner"
"github.com/navidrome/navidrome/server"
"github.com/navidrome/navidrome/server/events"
"github.com/navidrome/navidrome/server/nativeapi"
"github.com/navidrome/navidrome/server/subsonic"
"sync"
)
// Injectors from wire_injectors.go:
func CreateServer(musicFolder string) *server.Server {
dataStore := persistence.New()
scannerScanner := scanner.New(dataStore)
serverServer := server.New(scannerScanner, dataStore)
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
serverServer := server.New(dataStore)
return serverServer
}
func CreateScanner(musicFolder string) *scanner.Scanner {
dataStore := persistence.New()
scannerScanner := scanner.New(dataStore)
return scannerScanner
}
func CreateAppRouter() *app.Router {
dataStore := persistence.New()
router := app.New(dataStore)
func CreateNativeAPIRouter() *nativeapi.Router {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
broker := events.GetBroker()
share := core.NewShare(dataStore)
router := nativeapi.New(dataStore, broker, share)
return router
}
func CreateSubsonicAPIRouter() (*subsonic.Router, error) {
dataStore := persistence.New()
browser := engine.NewBrowser(dataStore)
imageCache, err := core.NewImageCache()
if err != nil {
return nil, err
}
cover := core.NewCover(dataStore, imageCache)
nowPlayingRepository := engine.NewNowPlayingRepository()
listGenerator := engine.NewListGenerator(dataStore, nowPlayingRepository)
users := engine.NewUsers(dataStore)
playlists := engine.NewPlaylists(dataStore)
ratings := engine.NewRatings(dataStore)
scrobbler := engine.NewScrobbler(dataStore, nowPlayingRepository)
search := engine.NewSearch(dataStore)
func CreateSubsonicAPIRouter() *subsonic.Router {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
artworkCache := core.GetImageCache()
artwork := core.NewArtwork(dataStore, artworkCache)
transcoderTranscoder := transcoder.New()
transcodingCache, err := core.NewTranscodingCache()
if err != nil {
return nil, err
}
transcodingCache := core.GetTranscodingCache()
mediaStreamer := core.NewMediaStreamer(dataStore, transcoderTranscoder, transcodingCache)
players := engine.NewPlayers(dataStore)
router := subsonic.New(browser, cover, listGenerator, users, playlists, ratings, scrobbler, search, mediaStreamer, players)
return router, nil
archiver := core.NewArchiver(dataStore)
players := core.NewPlayers(dataStore)
agentsAgents := agents.New(dataStore)
externalMetadata := core.NewExternalMetadata(dataStore, agentsAgents)
scanner := GetScanner()
broker := events.GetBroker()
playTracker := scrobbler.GetPlayTracker(dataStore, broker)
router := subsonic.New(dataStore, artwork, mediaStreamer, archiver, players, externalMetadata, scanner, broker, playTracker)
return router
}
func CreateLastFMRouter() *lastfm.Router {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
router := lastfm.NewRouter(dataStore)
return router
}
func createScanner() scanner.Scanner {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
artworkCache := core.GetImageCache()
artwork := core.NewArtwork(dataStore, artworkCache)
cacheWarmer := core.NewCacheWarmer(artwork, artworkCache)
broker := events.GetBroker()
scannerScanner := scanner.New(dataStore, cacheWarmer, broker)
return scannerScanner
}
// wire_injectors.go:
var allProviders = wire.NewSet(engine.Set, core.Set, scanner.New, subsonic.New, app.New, persistence.New)
var allProviders = wire.NewSet(core.Set, subsonic.New, nativeapi.New, persistence.New, lastfm.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
}

View File

@@ -1,25 +1,31 @@
//+build wireinject
//go:build wireinject
// +build wireinject
package cmd
import (
"github.com/deluan/navidrome/core"
"github.com/deluan/navidrome/engine"
"github.com/deluan/navidrome/persistence"
"github.com/deluan/navidrome/scanner"
"github.com/deluan/navidrome/server"
"github.com/deluan/navidrome/server/app"
"github.com/deluan/navidrome/server/subsonic"
"sync"
"github.com/google/wire"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/agents/lastfm"
"github.com/navidrome/navidrome/db"
"github.com/navidrome/navidrome/persistence"
"github.com/navidrome/navidrome/scanner"
"github.com/navidrome/navidrome/server"
"github.com/navidrome/navidrome/server/events"
"github.com/navidrome/navidrome/server/nativeapi"
"github.com/navidrome/navidrome/server/subsonic"
)
var allProviders = wire.NewSet(
engine.Set,
core.Set,
scanner.New,
subsonic.New,
app.New,
nativeapi.New,
persistence.New,
lastfm.NewRouter,
events.GetBroker,
db.Db,
)
func CreateServer(musicFolder string) *server.Server {
@@ -29,16 +35,41 @@ func CreateServer(musicFolder string) *server.Server {
))
}
func CreateScanner(musicFolder string) *scanner.Scanner {
func CreateNativeAPIRouter() *nativeapi.Router {
panic(wire.Build(
allProviders,
))
}
func CreateAppRouter() *app.Router {
panic(wire.Build(allProviders))
func CreateSubsonicAPIRouter() *subsonic.Router {
panic(wire.Build(
allProviders,
GetScanner,
))
}
func CreateSubsonicAPIRouter() (*subsonic.Router, error) {
panic(wire.Build(allProviders))
func CreateLastFMRouter() *lastfm.Router {
panic(wire.Build(
allProviders,
))
}
// 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,
))
}

View File

@@ -4,10 +4,13 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/log"
"github.com/kr/pretty"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log"
"github.com/robfig/cron/v3"
"github.com/spf13/viper"
)
@@ -20,30 +23,80 @@ type configOptions struct {
DbPath string
LogLevel string
ScanInterval time.Duration
ScanSchedule string
SessionTimeout time.Duration
BaseURL string
UILoginBackgroundURL string
EnableTranscodingConfig bool
EnableDownloads bool
TranscodingCacheSize string
ImageCacheSize string
AutoImportPlaylists bool
PlaylistsPath string
IgnoredArticles string
IndexGroups string
ProbeCommand string
CoverArtPriority string
CoverJpegQuality int
UIWelcomeMessage string
GATrackingID string
AuthRequestLimit int
AuthWindowLength time.Duration
SearchFullString bool
RecentlyAddedByModTime bool
IgnoredArticles string
IndexGroups string
ProbeCommand string
CoverArtPriority string
CoverJpegQuality int
UIWelcomeMessage string
EnableGravatar bool
EnableFavourites bool
EnableStarRating bool
EnableUserEditing bool
DefaultTheme string
EnableCoverAnimation bool
GATrackingID string
EnableLogRedacting bool
AuthRequestLimit int
AuthWindowLength time.Duration
PasswordEncryptionKey string
ReverseProxyUserHeader string
ReverseProxyWhitelist string
Scanner scannerOptions
Agents string
LastFM lastfmOptions
Spotify spotifyOptions
// DevFlags. These are used to enable/disable debugging and incomplete features
DevLogSourceLine bool
DevLogLevels map[string]string
DevAutoCreateAdminPassword string
DevOldScanner bool
DevAutoLoginUsername string
DevPreCacheAlbumArtwork bool
DevFastAccessCoverArt bool
DevActivityPanel bool
DevEnableShare bool
DevSidebarPlaylists bool
DevEnableBufferedScrobble bool
DevShowArtistPage bool
}
var Server = &configOptions{}
type scannerOptions struct {
Extractor string
GenreSeparators string
}
type lastfmOptions struct {
Enabled bool
ApiKey string
Secret string
Language string
}
type spotifyOptions struct {
ID string
Secret string
}
var (
Server = &configOptions{}
hooks []func()
)
func LoadFromFile(confFile string) {
viper.SetConfigFile(confFile)
@@ -53,12 +106,12 @@ func LoadFromFile(confFile string) {
func Load() {
err := viper.Unmarshal(&Server)
if err != nil {
fmt.Println("Error parsing config:", err)
fmt.Println("FATAL: Error parsing config:", err)
os.Exit(1)
}
err = os.MkdirAll(Server.DataFolder, os.ModePerm)
if err != nil {
fmt.Println("Error creating data path:", "path", Server.DataFolder, err)
fmt.Println("FATAL: Error creating data path:", "path", Server.DataFolder, err)
os.Exit(1)
}
Server.ConfigFile = viper.GetViper().ConfigFileUsed()
@@ -67,42 +120,131 @@ func Load() {
}
log.SetLevelString(Server.LogLevel)
log.SetLogLevels(Server.DevLogLevels)
log.SetLogSourceLine(Server.DevLogSourceLine)
log.Debug("Loaded configuration", "file", Server.ConfigFile, "config", fmt.Sprintf("%#v", Server))
log.SetRedacting(Server.EnableLogRedacting)
if err := validateScanSchedule(); err != nil {
os.Exit(1)
}
// Print current configuration if log level is Debug
if log.CurrentLevel() >= log.LevelDebug {
prettyConf := pretty.Sprintf("Loaded configuration from '%s': %# v", Server.ConfigFile, Server)
if Server.EnableLogRedacting {
prettyConf = log.Redact(prettyConf)
}
fmt.Println(prettyConf)
}
// Call init hooks
for _, hook := range hooks {
hook()
}
}
func validateScanSchedule() error {
if Server.ScanInterval != -1 {
log.Warn("ScanInterval is DEPRECATED. Please use ScanSchedule. See docs at https://navidrome.org/docs/usage/configuration-options/")
if Server.ScanSchedule != "@every 1m" {
log.Error("You cannot specify both ScanInterval and ScanSchedule, ignoring ScanInterval")
} else {
if Server.ScanInterval == 0 {
Server.ScanSchedule = ""
} else {
Server.ScanSchedule = fmt.Sprintf("@every %s", Server.ScanInterval)
}
log.Warn("Setting ScanSchedule", "schedule", Server.ScanSchedule)
}
}
if Server.ScanSchedule == "0" || Server.ScanSchedule == "" {
Server.ScanSchedule = ""
return nil
}
if _, err := time.ParseDuration(Server.ScanSchedule); err == nil {
Server.ScanSchedule = "@every " + Server.ScanSchedule
}
c := cron.New()
_, err := c.AddFunc(Server.ScanSchedule, func() {})
if err != nil {
log.Error("Invalid ScanSchedule. Please read format spec at https://pkg.go.dev/github.com/robfig/cron#hdr-CRON_Expression_Format", "schedule", Server.ScanSchedule, err)
}
return err
}
// AddHook is used to register initialization code that should run as soon as the config is loaded
func AddHook(hook func()) {
hooks = append(hooks, hook)
}
func init() {
viper.SetDefault("musicfolder", "./music")
viper.SetDefault("datafolder", "./")
viper.SetDefault("musicfolder", filepath.Join(".", "music"))
viper.SetDefault("datafolder", ".")
viper.SetDefault("loglevel", "info")
viper.SetDefault("address", "0.0.0.0")
viper.SetDefault("port", 4533)
viper.SetDefault("sessiontimeout", consts.DefaultSessionTimeout)
viper.SetDefault("scaninterval", time.Minute)
viper.SetDefault("scaninterval", -1)
viper.SetDefault("scanschedule", "@every 1m")
viper.SetDefault("baseurl", "")
viper.SetDefault("uiloginbackgroundurl", "")
viper.SetDefault("uiloginbackgroundurl", consts.DefaultUILoginBackgroundURL)
viper.SetDefault("enabletranscodingconfig", false)
viper.SetDefault("transcodingcachesize", "100MB")
viper.SetDefault("imagecachesize", "100MB")
viper.SetDefault("autoimportplaylists", true)
viper.SetDefault("playlistspath", consts.DefaultPlaylistsPath)
viper.SetDefault("enabledownloads", true)
// Config options only valid for file/env configuration
viper.SetDefault("searchfullstring", false)
viper.SetDefault("recentlyaddedbymodtime", 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("probecommand", "ffmpeg %s -f ffmetadata")
viper.SetDefault("coverartpriority", "embedded, cover.*, folder.*, front.*")
viper.SetDefault("coverjpegquality", 75)
viper.SetDefault("uiwelcomemessage", "")
viper.SetDefault("enablegravatar", false)
viper.SetDefault("enablefavourites", true)
viper.SetDefault("enablestarrating", true)
viper.SetDefault("enableuserediting", true)
viper.SetDefault("defaulttheme", "Dark")
viper.SetDefault("enablecoveranimation", true)
viper.SetDefault("gatrackingid", "")
viper.SetDefault("enablelogredacting", true)
viper.SetDefault("authrequestlimit", 5)
viper.SetDefault("authwindowlength", 20*time.Second)
viper.SetDefault("passwordencryptionkey", "")
viper.SetDefault("reverseproxyuserheader", "Remote-User")
viper.SetDefault("reverseproxywhitelist", "")
viper.SetDefault("scanner.extractor", consts.DefaultScannerExtractor)
viper.SetDefault("scanner.genreseparators", ";/,")
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("spotify.id", "")
viper.SetDefault("spotify.secret", "")
// DevFlags. These are used to enable/disable debugging and incomplete features
viper.SetDefault("devlogsourceline", false)
viper.SetDefault("devautocreateadminpassword", "")
viper.SetDefault("devoldscanner", false)
viper.SetDefault("devautologinusername", "")
viper.SetDefault("devprecachealbumartwork", false)
viper.SetDefault("devfastaccesscoverart", false)
viper.SetDefault("devactivitypanel", true)
viper.SetDefault("devenableshare", false)
viper.SetDefault("devenablebufferedscrobble", true)
viper.SetDefault("devsidebarplaylists", true)
viper.SetDefault("devshowartistpage", false)
}
func InitConfig(cfgFile string) {
cfgFile = getConfigFile(cfgFile)
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
@@ -114,11 +256,20 @@ func InitConfig(cfgFile string) {
_ = viper.BindEnv("port")
viper.SetEnvPrefix("ND")
replacer := strings.NewReplacer(".", "_")
viper.SetEnvKeyReplacer(replacer)
viper.AutomaticEnv()
err := viper.ReadInConfig()
if cfgFile != "" && err != nil {
fmt.Println("Navidrome could not open config file: ", err)
if viper.ConfigFileUsed() != "" && err != nil {
fmt.Println("FATAL: Navidrome could not open config file: ", err)
os.Exit(1)
}
}
func getConfigFile(cfgFile string) string {
if cfgFile != "" {
return cfgFile
}
return os.Getenv("ND_CONFIGFILE")
}

View File

@@ -5,16 +5,15 @@ import (
"strings"
"unicode"
"github.com/deluan/navidrome/resources"
"github.com/navidrome/navidrome/resources"
)
func getBanner() string {
func loadBanner() string {
data, _ := resources.Asset("banner.txt")
return strings.TrimRightFunc(string(data), unicode.IsSpace)
}
func Banner() string {
version := "Version: " + Version()
padding := strings.Repeat(" ", 52-len(version))
return fmt.Sprintf("%s\n%s%s\n", getBanner(), padding, version)
return fmt.Sprintf("%s\n%52s\n", loadBanner(), version)
}

View File

@@ -3,6 +3,7 @@ package consts
import (
"crypto/md5"
"fmt"
"path/filepath"
"strings"
"time"
)
@@ -10,28 +11,45 @@ import (
const (
AppName = "navidrome"
LocalConfigFile = "./navidrome.toml"
DefaultDbPath = "navidrome.db?cache=shared&_busy_timeout=15000&_journal_mode=WAL&_foreign_keys=on"
InitialSetupFlagKey = "InitialSetup"
UIAuthorizationHeader = "X-ND-Authorization"
JWTSecretKey = "JWTSecret"
JWTIssuer = "ND"
DefaultSessionTimeout = 24 * time.Hour
UIAuthorizationHeader = "X-ND-Authorization"
UIClientUniqueIDHeader = "X-ND-Client-Unique-Id"
JWTSecretKey = "JWTSecret"
JWTIssuer = "ND"
DefaultSessionTimeout = 24 * time.Hour
CookieExpiry = 365 * 24 * 3600 // One year
// DefaultEncryptionKey This is the encryption key used if none is specified in the `PasswordEncryptionKey` option
// Never ever change this! Or it will break all Navidrome installations that don't set the config option
DefaultEncryptionKey = "just for obfuscation"
PasswordsEncryptedKey = "PasswordsEncryptedKey"
DevInitialUserName = "admin"
DevInitialName = "Dev Admin"
URLPathUI = "/app"
URLPathNativeAPI = "/api"
URLPathSubsonicAPI = "/rest"
// Login backgrounds from https://unsplash.com/collections/20072696/navidrome
DefaultUILoginBackgroundURL = "https://source.unsplash.com/collection/20072696/1600x900"
RequestThrottleBacklogLimit = 100
RequestThrottleBacklogTimeout = time.Minute
ArtistInfoTimeToLive = 3 * 24 * time.Hour
I18nFolder = "i18n"
SkipScanFile = ".ndignore"
PlaceholderAlbumArt = "navidrome-600x600.png"
PlaceholderAvatar = "logo-192x192.png"
DefaultHttpClientTimeOut = 10 * time.Second
DefaultScannerExtractor = "taglib"
)
// Cache options
@@ -46,6 +64,12 @@ const (
DefaultCacheCleanUpInterval = 10 * time.Minute
)
// Shared secrets (only add here "secrets" that can be public)
const (
LastFMAPIKey = "9b94a5515ea66b2da3ec03c12300327e"
LastFMAPISecret = "74cb6557cec7171d921af5d7d887c587" // nolint:gosec
)
var (
DefaultTranscodings = []map[string]interface{}{
{
@@ -56,15 +80,20 @@ var (
},
{
"name": "opus audio",
"targetFormat": "oga",
"targetFormat": "opus",
"defaultBitRate": 128,
"command": "ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -c:a libopus -f opus -",
},
}
DefaultPlaylistsPath = strings.Join([]string{".", "**/**"}, string(filepath.ListSeparator))
)
var (
VariousArtists = "Various Artists"
VariousArtistsID = fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(VariousArtists))))
UnknownArtist = "[Unknown Artist]"
VariousArtists = "Various Artists"
VariousArtistsID = fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(VariousArtists))))
UnknownArtist = "[Unknown Artist]"
VariousArtistsMbzId = "89ad4ac3-39f7-470e-963a-56509c546377"
ServerStart = time.Now()
)

View File

@@ -1,35 +1,59 @@
package consts
import "mime"
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},
}
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() {
mt := map[string]string{
".mp3": "audio/mpeg",
".ogg": "audio/ogg",
".oga": "audio/ogg",
".opus": "audio/ogg",
".aac": "audio/mp4",
".m4a": "audio/mp4",
".m4b": "audio/mp4",
".flac": "audio/flac",
".wav": "audio/x-wav",
".wma": "audio/x-ms-wma",
".ape": "audio/x-monkeys-audio",
".mpc": "audio/x-musepack",
".shn": "audio/x-shn",
".aif": "audio/x-aiff",
".aiff": "audio/x-aiff",
".m3u": "audio/x-mpegurl",
".pls": "audio/x-scpls",
".dsf": "audio/dsd",
".gif": "image/gif",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".bmp": "image/bmp",
for ext, fmt := range audioFormats {
_ = mime.AddExtensionType(ext, fmt.typ)
if fmt.lossless {
LosslessFormats = append(LosslessFormats, strings.TrimPrefix(ext, "."))
}
}
for ext, typ := range mt {
sort.Strings(LosslessFormats)
for ext, typ := range imageFormats {
_ = mime.AddExtensionType(ext, typ)
}
}

View File

@@ -1,6 +1,9 @@
package consts
import "fmt"
import (
"fmt"
"strings"
)
var (
// This will be set in build time. If not, version will be set to "dev"
@@ -11,10 +14,12 @@ var (
// Formats:
// dev
// v0.2.0 (5b84188)
// v0.3.2-SNAPSHOT (715f552)
// master (9ed35cb)
func Version() string {
if gitSha == "" {
return "dev"
}
gitTag = strings.TrimPrefix(gitTag, "v")
return fmt.Sprintf("%s (%s)", gitTag, gitSha)
}

52
contrib/freebsd_rc Normal file
View File

@@ -0,0 +1,52 @@
#!/bin/sh
#
# $FreeBSD: $
#
# PROVIDE: navidrome
# REQUIRE: NETWORKING
# KEYWORD:
#
# Add the following lines to /etc/rc.conf to enable navidrome:
# navidrome_enable="YES"
#
# navidrome_enable (bool): Set to YES to enable navidrome
# Default: NO
# navidrome_config (str): navidrome configration file
# Default: /usr/local/etc/navidrome/config.toml
# navidrome_datafolder (str): navidrome Folder to store application data
# Default: www
# navidrome_user (str): navidrome daemon user
# Default: www
# navidrome_group (str): navidrome daemon group
# Default: www
. /etc/rc.subr
name="navidrome"
rcvar="navidrome_enable"
load_rc_config $name
: ${navidrome_user:="www"}
: ${navidrome_group:="www"}
: ${navidrome_enable:="NO"}
: ${navidrome_config:="/usr/local/etc/navidrome/config.toml"}
: ${navidrome_flags=""}
: ${navidrome_facility:="daemon"}
: ${navidrome_priority:="debug"}
: ${navidrome_datafolder:="/var/db/${name}"}
required_dirs=${navidrome_datafolder}
required_files=${navidrome_config}
procname="/usr/local/bin/${name}"
pidfile="/var/run/${name}.pid"
start_precmd="${name}_precmd"
command=/usr/sbin/daemon
command_args="-S -l ${navidrome_facility} -s ${navidrome_priority} -T ${name} -t ${name} -p ${pidfile} \
${procname} --configfile ${navidrome_config} --datafolder ${navidrome_datafolder} ${navidrome_flags}"
navidrome_precmd()
{
install -o ${navidrome_user} /dev/null ${pidfile}
}
run_rc_command "$1"

View File

@@ -3,7 +3,6 @@
[Unit]
Description=Navidrome Music Server and Streamer compatible with Subsonic/Airsonic
After=remote-fs.target network.target
AssertPathExists=/var/lib/navidrome
[Install]
WantedBy=multi-user.target
@@ -13,6 +12,7 @@ User=navidrome
Group=navidrome
Type=simple
ExecStart=/usr/bin/navidrome
StateDirectory=navidrome
WorkingDirectory=/var/lib/navidrome
TimeoutStopSec=20
KillMode=process
@@ -21,18 +21,25 @@ Restart=on-failure
EnvironmentFile=-/etc/sysconfig/navidrome
# See https://www.freedesktop.org/software/systemd/man/systemd.exec.html
CapabilityBoundingSet=
DevicePolicy=closed
NoNewPrivileges=yes
LockPersonality=yes
PrivateTmp=yes
PrivateUsers=yes
ProtectControlGroups=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectClock=yes
ProtectHostname=yes
ProtectKernelLogs=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=yes
RestrictRealtime=yes
SystemCallFilter=~@clock @debug @module @mount @obsolete @privileged @reboot @setuid @swap
ReadWritePaths=/var/lib/navidrome
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
SystemCallArchitectures=native
UMask=0066
# You can uncomment the following line if you're not using the jukebox This
# will prevent navidrome from accessing any real (physical) devices

12
core/agents/README.md Normal file
View File

@@ -0,0 +1,12 @@
This folder abstracts metadata lookup into "agents". Each agent can be implemented to get as
much info as the external source provides, by using a granular set of interfaces
(see [interfaces](interfaces.go)].
A new agent must comply with these simple implementation rules:
1) Implement the `AgentName()` method. It just returns the name of the agent for logging purposes.
2) Implement one or more of the `*Retriever()` interfaces. That's where the agent's logic resides.
3) Register itself (in its `init()` function).
For an agent to be used it needs to be listed in the `Agents` config option (default is `"lastfm,spotify"`). The order dictates the priority of the agents
For a simple Agent example, look at the [placeholders.go](placeholders.go) agent source code.

161
core/agents/agents.go Normal file
View File

@@ -0,0 +1,161 @@
package agents
import (
"context"
"strings"
"time"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/utils"
)
type Agents struct {
ds model.DataStore
agents []Interface
}
func New(ds model.DataStore) *Agents {
order := strings.Split(conf.Server.Agents, ",")
order = append(order, PlaceholderAgentName)
var res []Interface
for _, name := range order {
init, ok := Map[name]
if !ok {
log.Error("Agent not available. Check configuration", "name", name)
continue
}
res = append(res, init(ds))
}
return &Agents{ds: ds, agents: res}
}
func (a *Agents) AgentName() string {
return "agents"
}
func (a *Agents) GetMBID(ctx context.Context, id string, name string) (string, error) {
start := time.Now()
for _, ag := range a.agents {
if utils.IsCtxDone(ctx) {
break
}
agent, ok := ag.(ArtistMBIDRetriever)
if !ok {
continue
}
mbid, err := agent.GetMBID(ctx, id, name)
if mbid != "" && err == nil {
log.Debug(ctx, "Got MBID", "agent", ag.AgentName(), "artist", name, "mbid", mbid, "elapsed", time.Since(start))
return mbid, err
}
}
return "", ErrNotFound
}
func (a *Agents) GetURL(ctx context.Context, id, name, mbid string) (string, error) {
start := time.Now()
for _, ag := range a.agents {
if utils.IsCtxDone(ctx) {
break
}
agent, ok := ag.(ArtistURLRetriever)
if !ok {
continue
}
url, err := agent.GetURL(ctx, id, name, mbid)
if url != "" && err == nil {
log.Debug(ctx, "Got External Url", "agent", ag.AgentName(), "artist", name, "url", url, "elapsed", time.Since(start))
return url, err
}
}
return "", ErrNotFound
}
func (a *Agents) GetBiography(ctx context.Context, id, name, mbid string) (string, error) {
start := time.Now()
for _, ag := range a.agents {
if utils.IsCtxDone(ctx) {
break
}
agent, ok := ag.(ArtistBiographyRetriever)
if !ok {
continue
}
bio, err := agent.GetBiography(ctx, id, name, mbid)
if bio != "" && err == nil {
log.Debug(ctx, "Got Biography", "agent", ag.AgentName(), "artist", name, "len", len(bio), "elapsed", time.Since(start))
return bio, err
}
}
return "", ErrNotFound
}
func (a *Agents) GetSimilar(ctx context.Context, id, name, mbid string, limit int) ([]Artist, error) {
start := time.Now()
for _, ag := range a.agents {
if utils.IsCtxDone(ctx) {
break
}
agent, ok := ag.(ArtistSimilarRetriever)
if !ok {
continue
}
similar, err := agent.GetSimilar(ctx, id, name, mbid, limit)
if len(similar) > 0 && err == nil {
log.Debug(ctx, "Got Similar Artists", "agent", ag.AgentName(), "artist", name, "similar", similar, "elapsed", time.Since(start))
return similar, err
}
}
return nil, ErrNotFound
}
func (a *Agents) GetImages(ctx context.Context, id, name, mbid string) ([]ArtistImage, error) {
start := time.Now()
for _, ag := range a.agents {
if utils.IsCtxDone(ctx) {
break
}
agent, ok := ag.(ArtistImageRetriever)
if !ok {
continue
}
images, err := agent.GetImages(ctx, id, name, mbid)
if len(images) > 0 && err == nil {
log.Debug(ctx, "Got Images", "agent", ag.AgentName(), "artist", name, "images", images, "elapsed", time.Since(start))
return images, err
}
}
return nil, ErrNotFound
}
func (a *Agents) GetTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]Song, error) {
start := time.Now()
for _, ag := range a.agents {
if utils.IsCtxDone(ctx) {
break
}
agent, ok := ag.(ArtistTopSongsRetriever)
if !ok {
continue
}
songs, err := agent.GetTopSongs(ctx, id, artistName, mbid, count)
if len(songs) > 0 && err == nil {
log.Debug(ctx, "Got Top Songs", "agent", ag.AgentName(), "artist", artistName, "songs", songs, "elapsed", time.Since(start))
return songs, err
}
}
return nil, ErrNotFound
}
var _ Interface = (*Agents)(nil)
var _ ArtistMBIDRetriever = (*Agents)(nil)
var _ ArtistURLRetriever = (*Agents)(nil)
var _ ArtistBiographyRetriever = (*Agents)(nil)
var _ ArtistSimilarRetriever = (*Agents)(nil)
var _ ArtistImageRetriever = (*Agents)(nil)
var _ ArtistTopSongsRetriever = (*Agents)(nil)

View File

@@ -0,0 +1,17 @@
package agents
import (
"testing"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestAgents(t *testing.T) {
tests.Init(t, false)
log.SetLevel(log.LevelCritical)
RegisterFailHandler(Fail)
RunSpecs(t, "Agents Test Suite")
}

249
core/agents/agents_test.go Normal file
View File

@@ -0,0 +1,249 @@
package agents
import (
"context"
"errors"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/tests"
"github.com/navidrome/navidrome/conf"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Agents", func() {
var ctx context.Context
var cancel context.CancelFunc
var ds model.DataStore
BeforeEach(func() {
ctx, cancel = context.WithCancel(context.Background())
ds = &tests.MockDataStore{}
})
Describe("Placeholder", func() {
var ag *Agents
BeforeEach(func() {
conf.Server.Agents = ""
ag = New(ds)
})
It("calls the placeholder GetBiography", func() {
Expect(ag.GetBiography(ctx, "123", "John Doe", "mb123")).To(Equal(placeholderBiography))
})
It("calls the placeholder GetImages", func() {
images, err := ag.GetImages(ctx, "123", "John Doe", "mb123")
Expect(err).ToNot(HaveOccurred())
Expect(images).To(HaveLen(3))
for _, i := range images {
Expect(i.URL).To(BeElementOf(placeholderArtistImageSmallUrl, placeholderArtistImageMediumUrl, placeholderArtistImageLargeUrl))
}
})
})
Describe("Agents", func() {
var ag *Agents
var mock *mockAgent
BeforeEach(func() {
mock = &mockAgent{}
Register("fake", func(ds model.DataStore) Interface {
return mock
})
Register("empty", func(ds model.DataStore) Interface {
return struct {
Interface
}{}
})
conf.Server.Agents = "empty,fake"
ag = New(ds)
Expect(ag.AgentName()).To(Equal("agents"))
})
Describe("GetMBID", func() {
It("returns on first match", func() {
Expect(ag.GetMBID(ctx, "123", "test")).To(Equal("mbid"))
Expect(mock.Args).To(ConsistOf("123", "test"))
})
It("skips the agent if it returns an error", func() {
mock.Err = errors.New("error")
_, err := ag.GetMBID(ctx, "123", "test")
Expect(err).To(MatchError(ErrNotFound))
Expect(mock.Args).To(ConsistOf("123", "test"))
})
It("interrupts if the context is canceled", func() {
cancel()
_, err := ag.GetMBID(ctx, "123", "test")
Expect(err).To(MatchError(ErrNotFound))
Expect(mock.Args).To(BeEmpty())
})
})
Describe("GetURL", func() {
It("returns on first match", func() {
Expect(ag.GetURL(ctx, "123", "test", "mb123")).To(Equal("url"))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
})
It("skips the agent if it returns an error", func() {
mock.Err = errors.New("error")
_, err := ag.GetURL(ctx, "123", "test", "mb123")
Expect(err).To(MatchError(ErrNotFound))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
})
It("interrupts if the context is canceled", func() {
cancel()
_, err := ag.GetURL(ctx, "123", "test", "mb123")
Expect(err).To(MatchError(ErrNotFound))
Expect(mock.Args).To(BeEmpty())
})
})
Describe("GetBiography", func() {
It("returns on first match", func() {
Expect(ag.GetBiography(ctx, "123", "test", "mb123")).To(Equal("bio"))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
})
It("skips the agent if it returns an error", func() {
mock.Err = errors.New("error")
Expect(ag.GetBiography(ctx, "123", "test", "mb123")).To(Equal(placeholderBiography))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
})
It("interrupts if the context is canceled", func() {
cancel()
_, err := ag.GetBiography(ctx, "123", "test", "mb123")
Expect(err).To(MatchError(ErrNotFound))
Expect(mock.Args).To(BeEmpty())
})
})
Describe("GetImages", func() {
It("returns on first match", func() {
Expect(ag.GetImages(ctx, "123", "test", "mb123")).To(Equal([]ArtistImage{{
URL: "imageUrl",
Size: 100,
}}))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
})
It("skips the agent if it returns an error", func() {
mock.Err = errors.New("error")
Expect(ag.GetImages(ctx, "123", "test", "mb123")).To(HaveLen(3))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
})
It("interrupts if the context is canceled", func() {
cancel()
_, err := ag.GetImages(ctx, "123", "test", "mb123")
Expect(err).To(MatchError(ErrNotFound))
Expect(mock.Args).To(BeEmpty())
})
})
Describe("GetSimilar", func() {
It("returns on first match", func() {
Expect(ag.GetSimilar(ctx, "123", "test", "mb123", 1)).To(Equal([]Artist{{
Name: "Joe Dohn",
MBID: "mbid321",
}}))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123", 1))
})
It("skips the agent if it returns an error", func() {
mock.Err = errors.New("error")
_, err := ag.GetSimilar(ctx, "123", "test", "mb123", 1)
Expect(err).To(MatchError(ErrNotFound))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123", 1))
})
It("interrupts if the context is canceled", func() {
cancel()
_, err := ag.GetSimilar(ctx, "123", "test", "mb123", 1)
Expect(err).To(MatchError(ErrNotFound))
Expect(mock.Args).To(BeEmpty())
})
})
Describe("GetTopSongs", func() {
It("returns on first match", func() {
Expect(ag.GetTopSongs(ctx, "123", "test", "mb123", 2)).To(Equal([]Song{{
Name: "A Song",
MBID: "mbid444",
}}))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123", 2))
})
It("skips the agent if it returns an error", func() {
mock.Err = errors.New("error")
_, err := ag.GetTopSongs(ctx, "123", "test", "mb123", 2)
Expect(err).To(MatchError(ErrNotFound))
Expect(mock.Args).To(ConsistOf("123", "test", "mb123", 2))
})
It("interrupts if the context is canceled", func() {
cancel()
_, err := ag.GetTopSongs(ctx, "123", "test", "mb123", 2)
Expect(err).To(MatchError(ErrNotFound))
Expect(mock.Args).To(BeEmpty())
})
})
})
})
type mockAgent struct {
Args []interface{}
Err error
}
func (a *mockAgent) AgentName() string {
return "fake"
}
func (a *mockAgent) GetMBID(ctx context.Context, id string, name string) (string, error) {
a.Args = []interface{}{id, name}
if a.Err != nil {
return "", a.Err
}
return "mbid", nil
}
func (a *mockAgent) GetURL(ctx context.Context, id, name, mbid string) (string, error) {
a.Args = []interface{}{id, name, mbid}
if a.Err != nil {
return "", a.Err
}
return "url", nil
}
func (a *mockAgent) GetBiography(ctx context.Context, id, name, mbid string) (string, error) {
a.Args = []interface{}{id, name, mbid}
if a.Err != nil {
return "", a.Err
}
return "bio", nil
}
func (a *mockAgent) GetImages(ctx context.Context, id, name, mbid string) ([]ArtistImage, error) {
a.Args = []interface{}{id, name, mbid}
if a.Err != nil {
return nil, a.Err
}
return []ArtistImage{{
URL: "imageUrl",
Size: 100,
}}, nil
}
func (a *mockAgent) GetSimilar(ctx context.Context, id, name, mbid string, limit int) ([]Artist, error) {
a.Args = []interface{}{id, name, mbid, limit}
if a.Err != nil {
return nil, a.Err
}
return []Artist{{
Name: "Joe Dohn",
MBID: "mbid321",
}}, nil
}
func (a *mockAgent) GetTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]Song, error) {
a.Args = []interface{}{id, artistName, mbid, count}
if a.Err != nil {
return nil, a.Err
}
return []Song{{
Name: "A Song",
MBID: "mbid444",
}}, nil
}

66
core/agents/interfaces.go Normal file
View File

@@ -0,0 +1,66 @@
package agents
import (
"context"
"errors"
"github.com/navidrome/navidrome/model"
)
type Constructor func(ds model.DataStore) Interface
type Interface interface {
AgentName() string
}
type Artist struct {
Name string
MBID string
}
type ArtistImage struct {
URL string
Size int
}
type Song struct {
Name string
MBID string
}
var (
ErrNotFound = errors.New("not found")
)
type ArtistMBIDRetriever interface {
GetMBID(ctx context.Context, id string, name string) (string, error)
}
type ArtistURLRetriever interface {
GetURL(ctx context.Context, id, name, mbid string) (string, error)
}
type ArtistBiographyRetriever interface {
GetBiography(ctx context.Context, id, name, mbid string) (string, error)
}
type ArtistSimilarRetriever interface {
GetSimilar(ctx context.Context, id, name, mbid string, limit int) ([]Artist, error)
}
type ArtistImageRetriever interface {
GetImages(ctx context.Context, id, name, mbid string) ([]ArtistImage, error)
}
type ArtistTopSongsRetriever interface {
GetTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]Song, error)
}
var Map map[string]Constructor
func Register(name string, init Constructor) {
if Map == nil {
Map = make(map[string]Constructor)
}
Map[name] = init
}

233
core/agents/lastfm/agent.go Normal file
View File

@@ -0,0 +1,233 @@
package lastfm
import (
"context"
"net/http"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/agents"
"github.com/navidrome/navidrome/core/scrobbler"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils"
)
const (
lastFMAgentName = "lastfm"
)
type lastfmAgent struct {
ds model.DataStore
sessionKeys *sessionKeys
apiKey string
secret string
lang string
client *Client
}
func lastFMConstructor(ds model.DataStore) *lastfmAgent {
l := &lastfmAgent{
ds: ds,
lang: conf.Server.LastFM.Language,
apiKey: conf.Server.LastFM.ApiKey,
secret: conf.Server.LastFM.Secret,
sessionKeys: &sessionKeys{ds: ds},
}
hc := &http.Client{
Timeout: consts.DefaultHttpClientTimeOut,
}
chc := utils.NewCachedHTTPClient(hc, consts.DefaultHttpClientTimeOut)
l.client = NewClient(l.apiKey, l.secret, l.lang, chc)
return l
}
func (l *lastfmAgent) AgentName() string {
return lastFMAgentName
}
func (l *lastfmAgent) GetMBID(ctx context.Context, id string, name string) (string, error) {
a, err := l.callArtistGetInfo(ctx, name, "")
if err != nil {
return "", err
}
if a.MBID == "" {
return "", agents.ErrNotFound
}
return a.MBID, nil
}
func (l *lastfmAgent) GetURL(ctx context.Context, id, name, mbid string) (string, error) {
a, err := l.callArtistGetInfo(ctx, name, mbid)
if err != nil {
return "", err
}
if a.URL == "" {
return "", agents.ErrNotFound
}
return a.URL, nil
}
func (l *lastfmAgent) GetBiography(ctx context.Context, id, name, mbid string) (string, error) {
a, err := l.callArtistGetInfo(ctx, name, mbid)
if err != nil {
return "", err
}
if a.Bio.Summary == "" {
return "", agents.ErrNotFound
}
return a.Bio.Summary, nil
}
func (l *lastfmAgent) GetSimilar(ctx context.Context, id, name, mbid string, limit int) ([]agents.Artist, error) {
resp, err := l.callArtistGetSimilar(ctx, name, mbid, limit)
if err != nil {
return nil, err
}
if len(resp) == 0 {
return nil, agents.ErrNotFound
}
var res []agents.Artist
for _, a := range resp {
res = append(res, agents.Artist{
Name: a.Name,
MBID: a.MBID,
})
}
return res, nil
}
func (l *lastfmAgent) GetTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]agents.Song, error) {
resp, err := l.callArtistGetTopTracks(ctx, artistName, mbid, count)
if err != nil {
return nil, err
}
if len(resp) == 0 {
return nil, agents.ErrNotFound
}
var res []agents.Song
for _, t := range resp {
res = append(res, agents.Song{
Name: t.Name,
MBID: t.MBID,
})
}
return res, nil
}
func (l *lastfmAgent) callArtistGetInfo(ctx context.Context, name string, mbid string) (*Artist, error) {
a, err := l.client.ArtistGetInfo(ctx, name, mbid)
lfErr, isLastFMError := err.(*lastFMError)
if mbid != "" && ((err == nil && a.Name == "[unknown]") || (isLastFMError && lfErr.Code == 6)) {
log.Warn(ctx, "LastFM/artist.getInfo could not find artist by mbid, trying again", "artist", name, "mbid", mbid)
return l.callArtistGetInfo(ctx, name, "")
}
if err != nil {
log.Error(ctx, "Error calling LastFM/artist.getInfo", "artist", name, "mbid", mbid, err)
return nil, err
}
return a, nil
}
func (l *lastfmAgent) callArtistGetSimilar(ctx context.Context, name string, mbid string, limit int) ([]Artist, error) {
s, err := l.client.ArtistGetSimilar(ctx, name, mbid, limit)
lfErr, isLastFMError := err.(*lastFMError)
if mbid != "" && ((err == nil && s.Attr.Artist == "[unknown]") || (isLastFMError && lfErr.Code == 6)) {
log.Warn(ctx, "LastFM/artist.getSimilar could not find artist by mbid, trying again", "artist", name, "mbid", mbid)
return l.callArtistGetSimilar(ctx, name, "", limit)
}
if err != nil {
log.Error(ctx, "Error calling LastFM/artist.getSimilar", "artist", name, "mbid", mbid, err)
return nil, err
}
return s.Artists, nil
}
func (l *lastfmAgent) callArtistGetTopTracks(ctx context.Context, artistName, mbid string, count int) ([]Track, error) {
t, err := l.client.ArtistGetTopTracks(ctx, artistName, mbid, count)
lfErr, isLastFMError := err.(*lastFMError)
if mbid != "" && ((err == nil && t.Attr.Artist == "[unknown]") || (isLastFMError && lfErr.Code == 6)) {
log.Warn(ctx, "LastFM/artist.getTopTracks could not find artist by mbid, trying again", "artist", artistName, "mbid", mbid)
return l.callArtistGetTopTracks(ctx, artistName, "", count)
}
if err != nil {
log.Error(ctx, "Error calling LastFM/artist.getTopTracks", "artist", artistName, "mbid", mbid, err)
return nil, err
}
return t.Track, nil
}
func (l *lastfmAgent) NowPlaying(ctx context.Context, userId string, track *model.MediaFile) error {
sk, err := l.sessionKeys.get(ctx, userId)
if err != nil || sk == "" {
return scrobbler.ErrNotAuthorized
}
err = l.client.UpdateNowPlaying(ctx, sk, ScrobbleInfo{
artist: track.Artist,
track: track.Title,
album: track.Album,
trackNumber: track.TrackNumber,
mbid: track.MbzTrackID,
duration: int(track.Duration),
albumArtist: track.AlbumArtist,
})
if err != nil {
log.Warn(ctx, "Last.fm client.updateNowPlaying returned error", "track", track.Title, err)
return scrobbler.ErrUnrecoverable
}
return nil
}
func (l *lastfmAgent) Scrobble(ctx context.Context, userId string, s scrobbler.Scrobble) error {
sk, err := l.sessionKeys.get(ctx, userId)
if err != nil || sk == "" {
return scrobbler.ErrNotAuthorized
}
if s.Duration <= 30 {
log.Debug(ctx, "Skipping Last.fm scrobble for short song", "track", s.Title, "duration", s.Duration)
return nil
}
err = l.client.Scrobble(ctx, sk, ScrobbleInfo{
artist: s.Artist,
track: s.Title,
album: s.Album,
trackNumber: s.TrackNumber,
mbid: s.MbzTrackID,
duration: int(s.Duration),
albumArtist: s.AlbumArtist,
timestamp: s.TimeStamp,
})
if err == nil {
return nil
}
lfErr, isLastFMError := err.(*lastFMError)
if !isLastFMError {
log.Warn(ctx, "Last.fm client.scrobble returned error", "track", s.Title, err)
return scrobbler.ErrRetryLater
}
if lfErr.Code == 11 || lfErr.Code == 16 {
return scrobbler.ErrRetryLater
}
return scrobbler.ErrUnrecoverable
}
func (l *lastfmAgent) IsAuthorized(ctx context.Context, userId string) bool {
sk, err := l.sessionKeys.get(ctx, userId)
return err == nil && sk != ""
}
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)
})
}
})
}

View File

@@ -0,0 +1,353 @@
package lastfm
import (
"bytes"
"context"
"errors"
"io"
"net/http"
"os"
"strconv"
"time"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/core/agents"
"github.com/navidrome/navidrome/core/scrobbler"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
const (
lastfmError3 = `{"error":3,"message":"Invalid Method - No method with that name in this package","links":[]}`
lastfmError6 = `{"error":6,"message":"The artist you supplied could not be found","links":[]}`
)
var _ = Describe("lastfmAgent", func() {
var ds model.DataStore
var ctx context.Context
BeforeEach(func() {
ds = &tests.MockDataStore{}
ctx = context.Background()
})
Describe("lastFMConstructor", func() {
It("uses configured api key and language", func() {
conf.Server.LastFM.ApiKey = "123"
conf.Server.LastFM.Secret = "secret"
conf.Server.LastFM.Language = "pt"
agent := lastFMConstructor(ds)
Expect(agent.apiKey).To(Equal("123"))
Expect(agent.secret).To(Equal("secret"))
Expect(agent.lang).To(Equal("pt"))
})
})
Describe("GetBiography", func() {
var agent *lastfmAgent
var httpClient *tests.FakeHttpClient
BeforeEach(func() {
httpClient = &tests.FakeHttpClient{}
client := NewClient("API_KEY", "SECRET", "pt", httpClient)
agent = lastFMConstructor(ds)
agent.client = client
})
It("returns the biography", func() {
f, _ := os.Open("tests/fixtures/lastfm.artist.getinfo.json")
httpClient.Res = http.Response{Body: f, StatusCode: 200}
Expect(agent.GetBiography(ctx, "123", "U2", "mbid-1234")).To(Equal("U2 é uma das mais importantes bandas de rock de todos os tempos. Formada em 1976 em Dublin, composta por Bono (vocalista e guitarrista), The Edge (guitarrista, pianista e backing vocal), Adam Clayton (baixista), Larry Mullen, Jr. (baterista e percussionista).\n\nDesde a década de 80, U2 é uma das bandas mais populares no mundo. Seus shows são únicos e um verdadeiro festival de efeitos especiais, além de serem um dos que mais arrecadam anualmente. <a href=\"https://www.last.fm/music/U2\">Read more on Last.fm</a>"))
Expect(httpClient.RequestCount).To(Equal(1))
Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234"))
})
It("returns an error if Last.FM call fails", func() {
httpClient.Err = errors.New("error")
_, err := agent.GetBiography(ctx, "123", "U2", "mbid-1234")
Expect(err).To(HaveOccurred())
Expect(httpClient.RequestCount).To(Equal(1))
Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234"))
})
It("returns an error if Last.FM call returns an error", func() {
httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError3)), StatusCode: 200}
_, err := agent.GetBiography(ctx, "123", "U2", "mbid-1234")
Expect(err).To(HaveOccurred())
Expect(httpClient.RequestCount).To(Equal(1))
Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234"))
})
It("returns an error if Last.FM call returns an error 6 and mbid is empty", func() {
httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError6)), StatusCode: 200}
_, err := agent.GetBiography(ctx, "123", "U2", "")
Expect(err).To(HaveOccurred())
Expect(httpClient.RequestCount).To(Equal(1))
})
Context("MBID non existent in Last.FM", func() {
It("calls again when the response is artist == [unknown]", func() {
f, _ := os.Open("tests/fixtures/lastfm.artist.getinfo.unknown.json")
httpClient.Res = http.Response{Body: f, StatusCode: 200}
_, _ = agent.GetBiography(ctx, "123", "U2", "mbid-1234")
Expect(httpClient.RequestCount).To(Equal(2))
Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(BeEmpty())
})
It("calls again when last.fm returns an error 6", func() {
httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError6)), StatusCode: 200}
_, _ = agent.GetBiography(ctx, "123", "U2", "mbid-1234")
Expect(httpClient.RequestCount).To(Equal(2))
Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(BeEmpty())
})
})
})
Describe("GetSimilar", func() {
var agent *lastfmAgent
var httpClient *tests.FakeHttpClient
BeforeEach(func() {
httpClient = &tests.FakeHttpClient{}
client := NewClient("API_KEY", "SECRET", "pt", httpClient)
agent = lastFMConstructor(ds)
agent.client = client
})
It("returns similar artists", func() {
f, _ := os.Open("tests/fixtures/lastfm.artist.getsimilar.json")
httpClient.Res = http.Response{Body: f, StatusCode: 200}
Expect(agent.GetSimilar(ctx, "123", "U2", "mbid-1234", 2)).To(Equal([]agents.Artist{
{Name: "Passengers", MBID: "e110c11f-1c94-4471-a350-c38f46b29389"},
{Name: "INXS", MBID: "481bf5f9-2e7c-4c44-b08a-05b32bc7c00d"},
}))
Expect(httpClient.RequestCount).To(Equal(1))
Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234"))
})
It("returns an error if Last.FM call fails", func() {
httpClient.Err = errors.New("error")
_, err := agent.GetSimilar(ctx, "123", "U2", "mbid-1234", 2)
Expect(err).To(HaveOccurred())
Expect(httpClient.RequestCount).To(Equal(1))
Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234"))
})
It("returns an error if Last.FM call returns an error", func() {
httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError3)), StatusCode: 200}
_, err := agent.GetSimilar(ctx, "123", "U2", "mbid-1234", 2)
Expect(err).To(HaveOccurred())
Expect(httpClient.RequestCount).To(Equal(1))
Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234"))
})
It("returns an error if Last.FM call returns an error 6 and mbid is empty", func() {
httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError6)), StatusCode: 200}
_, err := agent.GetSimilar(ctx, "123", "U2", "", 2)
Expect(err).To(HaveOccurred())
Expect(httpClient.RequestCount).To(Equal(1))
})
Context("MBID non existent in Last.FM", func() {
It("calls again when the response is artist == [unknown]", func() {
f, _ := os.Open("tests/fixtures/lastfm.artist.getsimilar.unknown.json")
httpClient.Res = http.Response{Body: f, StatusCode: 200}
_, _ = agent.GetSimilar(ctx, "123", "U2", "mbid-1234", 2)
Expect(httpClient.RequestCount).To(Equal(2))
Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(BeEmpty())
})
It("calls again when last.fm returns an error 6", func() {
httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError6)), StatusCode: 200}
_, _ = agent.GetSimilar(ctx, "123", "U2", "mbid-1234", 2)
Expect(httpClient.RequestCount).To(Equal(2))
Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(BeEmpty())
})
})
})
Describe("GetTopSongs", func() {
var agent *lastfmAgent
var httpClient *tests.FakeHttpClient
BeforeEach(func() {
httpClient = &tests.FakeHttpClient{}
client := NewClient("API_KEY", "SECRET", "pt", httpClient)
agent = lastFMConstructor(ds)
agent.client = client
})
It("returns top songs", func() {
f, _ := os.Open("tests/fixtures/lastfm.artist.gettoptracks.json")
httpClient.Res = http.Response{Body: f, StatusCode: 200}
Expect(agent.GetTopSongs(ctx, "123", "U2", "mbid-1234", 2)).To(Equal([]agents.Song{
{Name: "Beautiful Day", MBID: "f7f264d0-a89b-4682-9cd7-a4e7c37637af"},
{Name: "With or Without You", MBID: "6b9a509f-6907-4a6e-9345-2f12da09ba4b"},
}))
Expect(httpClient.RequestCount).To(Equal(1))
Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234"))
})
It("returns an error if Last.FM call fails", func() {
httpClient.Err = errors.New("error")
_, err := agent.GetTopSongs(ctx, "123", "U2", "mbid-1234", 2)
Expect(err).To(HaveOccurred())
Expect(httpClient.RequestCount).To(Equal(1))
Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234"))
})
It("returns an error if Last.FM call returns an error", func() {
httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError3)), StatusCode: 200}
_, err := agent.GetTopSongs(ctx, "123", "U2", "mbid-1234", 2)
Expect(err).To(HaveOccurred())
Expect(httpClient.RequestCount).To(Equal(1))
Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234"))
})
It("returns an error if Last.FM call returns an error 6 and mbid is empty", func() {
httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError6)), StatusCode: 200}
_, err := agent.GetTopSongs(ctx, "123", "U2", "", 2)
Expect(err).To(HaveOccurred())
Expect(httpClient.RequestCount).To(Equal(1))
})
Context("MBID non existent in Last.FM", func() {
It("calls again when the response is artist == [unknown]", func() {
f, _ := os.Open("tests/fixtures/lastfm.artist.gettoptracks.unknown.json")
httpClient.Res = http.Response{Body: f, StatusCode: 200}
_, _ = agent.GetTopSongs(ctx, "123", "U2", "mbid-1234", 2)
Expect(httpClient.RequestCount).To(Equal(2))
Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(BeEmpty())
})
It("calls again when last.fm returns an error 6", func() {
httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError6)), StatusCode: 200}
_, _ = agent.GetTopSongs(ctx, "123", "U2", "mbid-1234", 2)
Expect(httpClient.RequestCount).To(Equal(2))
Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(BeEmpty())
})
})
})
Describe("Scrobbling", func() {
var agent *lastfmAgent
var httpClient *tests.FakeHttpClient
var track *model.MediaFile
BeforeEach(func() {
_ = ds.UserProps(ctx).Put("user-1", sessionKeyProperty, "SK-1")
httpClient = &tests.FakeHttpClient{}
client := NewClient("API_KEY", "SECRET", "en", httpClient)
agent = lastFMConstructor(ds)
agent.client = client
track = &model.MediaFile{
ID: "123",
Title: "Track Title",
Album: "Track Album",
Artist: "Track Artist",
AlbumArtist: "Track AlbumArtist",
TrackNumber: 1,
Duration: 180,
MbzTrackID: "mbz-123",
}
})
Describe("NowPlaying", func() {
It("calls Last.fm with correct params", func() {
httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString("{}")), StatusCode: 200}
err := agent.NowPlaying(ctx, "user-1", track)
Expect(err).ToNot(HaveOccurred())
Expect(httpClient.SavedRequest.Method).To(Equal(http.MethodPost))
sentParams := httpClient.SavedRequest.URL.Query()
Expect(sentParams.Get("method")).To(Equal("track.updateNowPlaying"))
Expect(sentParams.Get("sk")).To(Equal("SK-1"))
Expect(sentParams.Get("track")).To(Equal(track.Title))
Expect(sentParams.Get("album")).To(Equal(track.Album))
Expect(sentParams.Get("artist")).To(Equal(track.Artist))
Expect(sentParams.Get("albumArtist")).To(Equal(track.AlbumArtist))
Expect(sentParams.Get("trackNumber")).To(Equal(strconv.Itoa(track.TrackNumber)))
Expect(sentParams.Get("duration")).To(Equal(strconv.FormatFloat(float64(track.Duration), 'G', -1, 32)))
Expect(sentParams.Get("mbid")).To(Equal(track.MbzTrackID))
})
It("returns ErrNotAuthorized if user is not linked", func() {
err := agent.NowPlaying(ctx, "user-2", track)
Expect(err).To(MatchError(scrobbler.ErrNotAuthorized))
})
})
Describe("Scrobble", func() {
It("calls Last.fm with correct params", func() {
ts := time.Now()
httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString("{}")), StatusCode: 200}
err := agent.Scrobble(ctx, "user-1", scrobbler.Scrobble{MediaFile: *track, TimeStamp: ts})
Expect(err).ToNot(HaveOccurred())
Expect(httpClient.SavedRequest.Method).To(Equal(http.MethodPost))
sentParams := httpClient.SavedRequest.URL.Query()
Expect(sentParams.Get("method")).To(Equal("track.scrobble"))
Expect(sentParams.Get("sk")).To(Equal("SK-1"))
Expect(sentParams.Get("track")).To(Equal(track.Title))
Expect(sentParams.Get("album")).To(Equal(track.Album))
Expect(sentParams.Get("artist")).To(Equal(track.Artist))
Expect(sentParams.Get("albumArtist")).To(Equal(track.AlbumArtist))
Expect(sentParams.Get("trackNumber")).To(Equal(strconv.Itoa(track.TrackNumber)))
Expect(sentParams.Get("duration")).To(Equal(strconv.FormatFloat(float64(track.Duration), 'G', -1, 32)))
Expect(sentParams.Get("mbid")).To(Equal(track.MbzTrackID))
Expect(sentParams.Get("timestamp")).To(Equal(strconv.FormatInt(ts.Unix(), 10)))
})
It("skips songs with less than 31 seconds", func() {
track.Duration = 29
httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString("{}")), StatusCode: 200}
err := agent.Scrobble(ctx, "user-1", scrobbler.Scrobble{MediaFile: *track, TimeStamp: time.Now()})
Expect(err).ToNot(HaveOccurred())
Expect(httpClient.SavedRequest).To(BeNil())
})
It("returns ErrNotAuthorized if user is not linked", func() {
err := agent.Scrobble(ctx, "user-2", scrobbler.Scrobble{MediaFile: *track, TimeStamp: time.Now()})
Expect(err).To(MatchError(scrobbler.ErrNotAuthorized))
})
It("returns ErrRetryLater on error 11", func() {
httpClient.Res = http.Response{
Body: io.NopCloser(bytes.NewBufferString(`{"error":11,"message":"Service Offline - This service is temporarily offline. Try again later."}`)),
StatusCode: 400,
}
err := agent.Scrobble(ctx, "user-1", scrobbler.Scrobble{MediaFile: *track, TimeStamp: time.Now()})
Expect(err).To(MatchError(scrobbler.ErrRetryLater))
})
It("returns ErrRetryLater on error 16", func() {
httpClient.Res = http.Response{
Body: io.NopCloser(bytes.NewBufferString(`{"error":16,"message":"There was a temporary error processing your request. Please try again"}`)),
StatusCode: 400,
}
err := agent.Scrobble(ctx, "user-1", scrobbler.Scrobble{MediaFile: *track, TimeStamp: time.Now()})
Expect(err).To(MatchError(scrobbler.ErrRetryLater))
})
It("returns ErrRetryLater on http errors", func() {
httpClient.Res = http.Response{
Body: io.NopCloser(bytes.NewBufferString(`internal server error`)),
StatusCode: 500,
}
err := agent.Scrobble(ctx, "user-1", scrobbler.Scrobble{MediaFile: *track, TimeStamp: time.Now()})
Expect(err).To(MatchError(scrobbler.ErrRetryLater))
})
It("returns ErrUnrecoverable on other errors", func() {
httpClient.Res = http.Response{
Body: io.NopCloser(bytes.NewBufferString(`{"error":8,"message":"Operation failed - Something else went wrong"}`)),
StatusCode: 400,
}
err := agent.Scrobble(ctx, "user-1", scrobbler.Scrobble{MediaFile: *track, TimeStamp: time.Now()})
Expect(err).To(MatchError(scrobbler.ErrUnrecoverable))
})
})
})
})

View File

@@ -0,0 +1,127 @@
package lastfm
import (
"bytes"
"context"
_ "embed"
"net/http"
"time"
"github.com/deluan/rest"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/server"
"github.com/navidrome/navidrome/utils"
)
//go:embed token_received.html
var tokenReceivedPage []byte
type Router struct {
http.Handler
ds model.DataStore
sessionKeys *sessionKeys
client *Client
apiKey string
secret string
}
func NewRouter(ds model.DataStore) *Router {
r := &Router{
ds: ds,
apiKey: conf.Server.LastFM.ApiKey,
secret: conf.Server.LastFM.Secret,
sessionKeys: &sessionKeys{ds: ds},
}
r.Handler = r.routes()
hc := &http.Client{
Timeout: consts.DefaultHttpClientTimeOut,
}
r.client = NewClient(r.apiKey, r.secret, "en", hc)
return r
}
func (s *Router) routes() http.Handler {
r := chi.NewRouter()
r.Group(func(r chi.Router) {
r.Use(server.Authenticator(s.ds))
r.Use(server.JWTRefresher)
r.Get("/link", s.getLinkStatus)
r.Delete("/link", s.unlink)
})
r.Get("/link/callback", s.callback)
return r
}
func (s *Router) getLinkStatus(w http.ResponseWriter, r *http.Request) {
resp := map[string]interface{}{"status": true}
u, _ := request.UserFrom(r.Context())
key, err := s.sessionKeys.get(r.Context(), u.ID)
if err != nil && err != model.ErrNotFound {
resp["error"] = err
resp["status"] = false
_ = rest.RespondWithJSON(w, http.StatusInternalServerError, resp)
return
}
resp["status"] = key != ""
_ = rest.RespondWithJSON(w, http.StatusOK, resp)
}
func (s *Router) unlink(w http.ResponseWriter, r *http.Request) {
u, _ := request.UserFrom(r.Context())
err := s.sessionKeys.delete(r.Context(), u.ID)
if err != nil {
_ = rest.RespondWithError(w, http.StatusInternalServerError, err.Error())
} else {
_ = rest.RespondWithJSON(w, http.StatusOK, map[string]string{})
}
}
func (s *Router) callback(w http.ResponseWriter, r *http.Request) {
token := utils.ParamString(r, "token")
if token == "" {
_ = rest.RespondWithError(w, http.StatusBadRequest, "token not received")
return
}
uid := utils.ParamString(r, "uid")
if uid == "" {
_ = rest.RespondWithError(w, http.StatusBadRequest, "uid not received")
return
}
// 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)
if err != nil {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("An error occurred while authorizing with Last.fm. \n\nRequest ID: " + middleware.GetReqID(ctx)))
return
}
http.ServeContent(w, r, "response", time.Now(), bytes.NewReader(tokenReceivedPage))
}
func (s *Router) fetchSessionKey(ctx context.Context, uid, token string) error {
sessionKey, err := s.client.GetSession(ctx, token)
if err != nil {
log.Error(ctx, "Could not fetch LastFM session key", "userId", uid, "token", token,
"requestId", middleware.GetReqID(ctx), err)
return err
}
err = s.sessionKeys.put(ctx, uid, sessionKey)
if err != nil {
log.Error("Could not save LastFM session key", "userId", uid, "requestId", middleware.GetReqID(ctx), err)
}
return err
}

View File

@@ -0,0 +1,221 @@
package lastfm
import (
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/utils"
)
const (
apiBaseUrl = "https://ws.audioscrobbler.com/2.0/"
)
type lastFMError struct {
Code int
Message string
}
func (e *lastFMError) Error() string {
return fmt.Sprintf("last.fm error(%d): %s", e.Code, e.Message)
}
type httpDoer interface {
Do(req *http.Request) (*http.Response, error)
}
func NewClient(apiKey string, secret string, lang string, hc httpDoer) *Client {
return &Client{apiKey, secret, lang, hc}
}
type Client struct {
apiKey string
secret string
lang string
hc httpDoer
}
func (c *Client) ArtistGetInfo(ctx context.Context, name string, mbid string) (*Artist, error) {
params := url.Values{}
params.Add("method", "artist.getInfo")
params.Add("artist", name)
params.Add("mbid", mbid)
params.Add("lang", c.lang)
response, err := c.makeRequest(http.MethodGet, params, false)
if err != nil {
return nil, err
}
return &response.Artist, nil
}
func (c *Client) ArtistGetSimilar(ctx context.Context, name string, mbid string, limit int) (*SimilarArtists, error) {
params := url.Values{}
params.Add("method", "artist.getSimilar")
params.Add("artist", name)
params.Add("mbid", mbid)
params.Add("limit", strconv.Itoa(limit))
response, err := c.makeRequest(http.MethodGet, params, false)
if err != nil {
return nil, err
}
return &response.SimilarArtists, nil
}
func (c *Client) ArtistGetTopTracks(ctx context.Context, name string, mbid string, limit int) (*TopTracks, error) {
params := url.Values{}
params.Add("method", "artist.getTopTracks")
params.Add("artist", name)
params.Add("mbid", mbid)
params.Add("limit", strconv.Itoa(limit))
response, err := c.makeRequest(http.MethodGet, params, false)
if err != nil {
return nil, err
}
return &response.TopTracks, nil
}
func (c *Client) GetToken(ctx context.Context) (string, error) {
params := url.Values{}
params.Add("method", "auth.getToken")
c.sign(params)
response, err := c.makeRequest(http.MethodGet, params, true)
if err != nil {
return "", err
}
return response.Token, nil
}
func (c *Client) GetSession(ctx context.Context, token string) (string, error) {
params := url.Values{}
params.Add("method", "auth.getSession")
params.Add("token", token)
response, err := c.makeRequest(http.MethodGet, params, true)
if err != nil {
return "", err
}
return response.Session.Key, nil
}
type ScrobbleInfo struct {
artist string
track string
album string
trackNumber int
mbid string
duration int
albumArtist string
timestamp time.Time
}
func (c *Client) UpdateNowPlaying(ctx context.Context, sessionKey string, info ScrobbleInfo) error {
params := url.Values{}
params.Add("method", "track.updateNowPlaying")
params.Add("artist", info.artist)
params.Add("track", info.track)
params.Add("album", info.album)
params.Add("trackNumber", strconv.Itoa(info.trackNumber))
params.Add("mbid", info.mbid)
params.Add("duration", strconv.Itoa(info.duration))
params.Add("albumArtist", info.albumArtist)
params.Add("sk", sessionKey)
resp, err := c.makeRequest(http.MethodPost, params, true)
if err != nil {
return err
}
if resp.NowPlaying.IgnoredMessage.Code != "0" {
log.Warn(ctx, "LastFM: NowPlaying was ignored", "code", resp.NowPlaying.IgnoredMessage.Code,
"text", resp.NowPlaying.IgnoredMessage.Text)
}
return nil
}
func (c *Client) Scrobble(ctx context.Context, sessionKey string, info ScrobbleInfo) error {
params := url.Values{}
params.Add("method", "track.scrobble")
params.Add("timestamp", strconv.FormatInt(info.timestamp.Unix(), 10))
params.Add("artist", info.artist)
params.Add("track", info.track)
params.Add("album", info.album)
params.Add("trackNumber", strconv.Itoa(info.trackNumber))
params.Add("mbid", info.mbid)
params.Add("duration", strconv.Itoa(info.duration))
params.Add("albumArtist", info.albumArtist)
params.Add("sk", sessionKey)
resp, err := c.makeRequest(http.MethodPost, params, true)
if err != nil {
return err
}
if resp.Scrobbles.Scrobble.IgnoredMessage.Code != "0" {
log.Warn(ctx, "LastFM: Scrobble was ignored", "code", resp.Scrobbles.Scrobble.IgnoredMessage.Code,
"text", resp.Scrobbles.Scrobble.IgnoredMessage.Text)
}
if resp.Scrobbles.Attr.Accepted != 1 {
log.Warn(ctx, "LastFM: Scrobble was not accepted", "code", resp.Scrobbles.Scrobble.IgnoredMessage.Code,
"text", resp.Scrobbles.Scrobble.IgnoredMessage.Text)
}
return nil
}
func (c *Client) makeRequest(method string, params url.Values, signed bool) (*Response, error) {
params.Add("format", "json")
params.Add("api_key", c.apiKey)
if signed {
c.sign(params)
}
req, _ := http.NewRequest(method, apiBaseUrl, nil)
req.URL.RawQuery = params.Encode()
resp, err := c.hc.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
decoder := json.NewDecoder(resp.Body)
var response Response
jsonErr := decoder.Decode(&response)
if resp.StatusCode != 200 && jsonErr != nil {
return nil, fmt.Errorf("last.fm http status: (%d)", resp.StatusCode)
}
if jsonErr != nil {
return nil, jsonErr
}
if response.Error != 0 {
return &response, &lastFMError{Code: response.Error, Message: response.Message}
}
return &response, nil
}
func (c *Client) sign(params url.Values) {
// the parameters must be in order before hashing
keys := make([]string, 0, len(params))
for k := range params {
if utils.StringInSlice(k, []string{"format", "callback"}) {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
msg := strings.Builder{}
for _, k := range keys {
msg.WriteString(k)
msg.WriteString(params[k][0])
}
msg.WriteString(c.secret)
hash := md5.Sum([]byte(msg.String()))
params.Add("api_sig", hex.EncodeToString(hash[:]))
}

View File

@@ -0,0 +1,161 @@
package lastfm
import (
"bytes"
"context"
"crypto/md5"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Client", func() {
var httpClient *tests.FakeHttpClient
var client *Client
BeforeEach(func() {
httpClient = &tests.FakeHttpClient{}
client = NewClient("API_KEY", "SECRET", "pt", httpClient)
})
Describe("ArtistGetInfo", func() {
It("returns an artist for a successful response", func() {
f, _ := os.Open("tests/fixtures/lastfm.artist.getinfo.json")
httpClient.Res = http.Response{Body: f, StatusCode: 200}
artist, err := client.ArtistGetInfo(context.Background(), "U2", "123")
Expect(err).To(BeNil())
Expect(artist.Name).To(Equal("U2"))
Expect(httpClient.SavedRequest.URL.String()).To(Equal(apiBaseUrl + "?api_key=API_KEY&artist=U2&format=json&lang=pt&mbid=123&method=artist.getInfo"))
})
It("fails if Last.FM returns an http status != 200", func() {
httpClient.Res = http.Response{
Body: io.NopCloser(bytes.NewBufferString(`Internal Server Error`)),
StatusCode: 500,
}
_, err := client.ArtistGetInfo(context.Background(), "U2", "123")
Expect(err).To(MatchError("last.fm http status: (500)"))
})
It("fails if Last.FM returns an http status != 200", func() {
httpClient.Res = http.Response{
Body: io.NopCloser(bytes.NewBufferString(`{"error":3,"message":"Invalid Method - No method with that name in this package"}`)),
StatusCode: 400,
}
_, err := client.ArtistGetInfo(context.Background(), "U2", "123")
Expect(err).To(MatchError(&lastFMError{Code: 3, Message: "Invalid Method - No method with that name in this package"}))
})
It("fails if Last.FM returns an error", func() {
httpClient.Res = http.Response{
Body: io.NopCloser(bytes.NewBufferString(`{"error":6,"message":"The artist you supplied could not be found"}`)),
StatusCode: 200,
}
_, err := client.ArtistGetInfo(context.Background(), "U2", "123")
Expect(err).To(MatchError(&lastFMError{Code: 6, Message: "The artist you supplied could not be found"}))
})
It("fails if HttpClient.Do() returns error", func() {
httpClient.Err = errors.New("generic error")
_, err := client.ArtistGetInfo(context.Background(), "U2", "123")
Expect(err).To(MatchError("generic error"))
})
It("fails if returned body is not a valid JSON", func() {
httpClient.Res = http.Response{
Body: io.NopCloser(bytes.NewBufferString(`<xml>NOT_VALID_JSON</xml>`)),
StatusCode: 200,
}
_, err := client.ArtistGetInfo(context.Background(), "U2", "123")
Expect(err).To(MatchError("invalid character '<' looking for beginning of value"))
})
})
Describe("ArtistGetSimilar", func() {
It("returns an artist for a successful response", func() {
f, _ := os.Open("tests/fixtures/lastfm.artist.getsimilar.json")
httpClient.Res = http.Response{Body: f, StatusCode: 200}
similar, err := client.ArtistGetSimilar(context.Background(), "U2", "123", 2)
Expect(err).To(BeNil())
Expect(len(similar.Artists)).To(Equal(2))
Expect(httpClient.SavedRequest.URL.String()).To(Equal(apiBaseUrl + "?api_key=API_KEY&artist=U2&format=json&limit=2&mbid=123&method=artist.getSimilar"))
})
})
Describe("ArtistGetTopTracks", func() {
It("returns top tracks for a successful response", func() {
f, _ := os.Open("tests/fixtures/lastfm.artist.gettoptracks.json")
httpClient.Res = http.Response{Body: f, StatusCode: 200}
top, err := client.ArtistGetTopTracks(context.Background(), "U2", "123", 2)
Expect(err).To(BeNil())
Expect(len(top.Track)).To(Equal(2))
Expect(httpClient.SavedRequest.URL.String()).To(Equal(apiBaseUrl + "?api_key=API_KEY&artist=U2&format=json&limit=2&mbid=123&method=artist.getTopTracks"))
})
})
Describe("GetToken", func() {
It("returns a token when the request is successful", func() {
httpClient.Res = http.Response{
Body: io.NopCloser(bytes.NewBufferString(`{"token":"TOKEN"}`)),
StatusCode: 200,
}
Expect(client.GetToken(context.Background())).To(Equal("TOKEN"))
queryParams := httpClient.SavedRequest.URL.Query()
Expect(queryParams.Get("method")).To(Equal("auth.getToken"))
Expect(queryParams.Get("format")).To(Equal("json"))
Expect(queryParams.Get("api_key")).To(Equal("API_KEY"))
Expect(queryParams.Get("api_sig")).ToNot(BeEmpty())
})
})
Describe("GetSession", func() {
It("returns a session key when the request is successful", func() {
httpClient.Res = http.Response{
Body: io.NopCloser(bytes.NewBufferString(`{"session":{"name":"Navidrome","key":"SESSION_KEY","subscriber":0}}`)),
StatusCode: 200,
}
Expect(client.GetSession(context.Background(), "TOKEN")).To(Equal("SESSION_KEY"))
queryParams := httpClient.SavedRequest.URL.Query()
Expect(queryParams.Get("method")).To(Equal("auth.getSession"))
Expect(queryParams.Get("format")).To(Equal("json"))
Expect(queryParams.Get("token")).To(Equal("TOKEN"))
Expect(queryParams.Get("api_key")).To(Equal("API_KEY"))
Expect(queryParams.Get("api_sig")).ToNot(BeEmpty())
})
})
Describe("sign", func() {
It("adds an api_sig param with the signature", func() {
params := url.Values{}
params.Add("d", "444")
params.Add("callback", "https://myserver.com")
params.Add("a", "111")
params.Add("format", "json")
params.Add("c", "333")
params.Add("b", "222")
client.sign(params)
Expect(params).To(HaveKey("api_sig"))
sig := params.Get("api_sig")
expected := fmt.Sprintf("%x", md5.Sum([]byte("a111b222c333d444SECRET")))
Expect(sig).To(Equal(expected))
})
})
})

View File

@@ -0,0 +1,17 @@
package lastfm
import (
"testing"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestLastFM(t *testing.T) {
tests.Init(t, false)
log.SetLevel(log.LevelCritical)
RegisterFailHandler(Fail)
RunSpecs(t, "LastFM Test Suite")
}

View File

@@ -0,0 +1,110 @@
package lastfm
type Response struct {
Artist Artist `json:"artist"`
SimilarArtists SimilarArtists `json:"similarartists"`
TopTracks TopTracks `json:"toptracks"`
Error int `json:"error"`
Message string `json:"message"`
Token string `json:"token"`
Session Session `json:"session"`
NowPlaying NowPlaying `json:"nowplaying"`
Scrobbles Scrobbles `json:"scrobbles"`
}
type Artist struct {
Name string `json:"name"`
MBID string `json:"mbid"`
URL string `json:"url"`
Image []ArtistImage `json:"image"`
Bio ArtistBio `json:"bio"`
}
type SimilarArtists struct {
Artists []Artist `json:"artist"`
Attr Attr `json:"@attr"`
}
type Attr struct {
Artist string `json:"artist"`
}
type ArtistImage struct {
URL string `json:"#text"`
Size string `json:"size"`
}
type ArtistBio struct {
Published string `json:"published"`
Summary string `json:"summary"`
Content string `json:"content"`
}
type Track struct {
Name string `json:"name"`
MBID string `json:"mbid"`
}
type TopTracks struct {
Track []Track `json:"track"`
Attr Attr `json:"@attr"`
}
type Session struct {
Name string `json:"name"`
Key string `json:"key"`
Subscriber int `json:"subscriber"`
}
type NowPlaying struct {
Artist struct {
Corrected string `json:"corrected"`
Text string `json:"#text"`
} `json:"artist"`
IgnoredMessage struct {
Code string `json:"code"`
Text string `json:"#text"`
} `json:"ignoredMessage"`
Album struct {
Corrected string `json:"corrected"`
Text string `json:"#text"`
} `json:"album"`
AlbumArtist struct {
Corrected string `json:"corrected"`
Text string `json:"#text"`
} `json:"albumArtist"`
Track struct {
Corrected string `json:"corrected"`
Text string `json:"#text"`
} `json:"track"`
}
type Scrobbles struct {
Attr struct {
Accepted int `json:"accepted"`
Ignored int `json:"ignored"`
} `json:"@attr"`
Scrobble struct {
Artist struct {
Corrected string `json:"corrected"`
Text string `json:"#text"`
} `json:"artist"`
IgnoredMessage struct {
Code string `json:"code"`
Text string `json:"#text"`
} `json:"ignoredMessage"`
AlbumArtist struct {
Corrected string `json:"corrected"`
Text string `json:"#text"`
} `json:"albumArtist"`
Timestamp string `json:"timestamp"`
Album struct {
Corrected string `json:"corrected"`
Text string `json:"#text"`
} `json:"album"`
Track struct {
Corrected string `json:"corrected"`
Text string `json:"#text"`
} `json:"track"`
} `json:"scrobble"`
}

View File

@@ -0,0 +1,65 @@
package lastfm
import (
"encoding/json"
"os"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("LastFM responses", func() {
Describe("Artist", func() {
It("parses the response correctly", func() {
var resp Response
body, _ := os.ReadFile("tests/fixtures/lastfm.artist.getinfo.json")
err := json.Unmarshal(body, &resp)
Expect(err).To(BeNil())
Expect(resp.Artist.Name).To(Equal("U2"))
Expect(resp.Artist.MBID).To(Equal("a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432"))
Expect(resp.Artist.URL).To(Equal("https://www.last.fm/music/U2"))
Expect(resp.Artist.Bio.Summary).To(ContainSubstring("U2 é uma das mais importantes bandas de rock de todos os tempos"))
})
})
Describe("SimilarArtists", func() {
It("parses the response correctly", func() {
var resp Response
body, _ := os.ReadFile("tests/fixtures/lastfm.artist.getsimilar.json")
err := json.Unmarshal(body, &resp)
Expect(err).To(BeNil())
Expect(resp.SimilarArtists.Artists).To(HaveLen(2))
Expect(resp.SimilarArtists.Artists[0].Name).To(Equal("Passengers"))
Expect(resp.SimilarArtists.Artists[1].Name).To(Equal("INXS"))
})
})
Describe("TopTracks", func() {
It("parses the response correctly", func() {
var resp Response
body, _ := os.ReadFile("tests/fixtures/lastfm.artist.gettoptracks.json")
err := json.Unmarshal(body, &resp)
Expect(err).To(BeNil())
Expect(resp.TopTracks.Track).To(HaveLen(2))
Expect(resp.TopTracks.Track[0].Name).To(Equal("Beautiful Day"))
Expect(resp.TopTracks.Track[0].MBID).To(Equal("f7f264d0-a89b-4682-9cd7-a4e7c37637af"))
Expect(resp.TopTracks.Track[1].Name).To(Equal("With or Without You"))
Expect(resp.TopTracks.Track[1].MBID).To(Equal("6b9a509f-6907-4a6e-9345-2f12da09ba4b"))
})
})
Describe("Error", func() {
It("parses the error response correctly", func() {
var error Response
body := []byte(`{"error":3,"message":"Invalid Method - No method with that name in this package"}`)
err := json.Unmarshal(body, &error)
Expect(err).To(BeNil())
Expect(error.Error).To(Equal(3))
Expect(error.Message).To(Equal("Invalid Method - No method with that name in this package"))
})
})
})

View File

@@ -0,0 +1,28 @@
package lastfm
import (
"context"
"github.com/navidrome/navidrome/model"
)
const (
sessionKeyProperty = "LastFMSessionKey"
)
// sessionKeys is a simple wrapper around the UserPropsRepository
type sessionKeys struct {
ds model.DataStore
}
func (sk *sessionKeys) put(ctx context.Context, userId, sessionKey string) error {
return sk.ds.UserProps(ctx).Put(userId, sessionKeyProperty, sessionKey)
}
func (sk *sessionKeys) get(ctx context.Context, userId string) (string, error) {
return sk.ds.UserProps(ctx).Get(userId, sessionKeyProperty)
}
func (sk *sessionKeys) delete(ctx context.Context, userId string) error {
return sk.ds.UserProps(ctx).Delete(userId, sessionKeyProperty)
}

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Account Linking Success</title>
</head>
<body>
<h2 id="msg"></h2>
<script>
setTimeout("document.getElementById('msg').innerHTML = 'Success! Your account is linked to Last.fm. You can close this tab now.';",2000)
document.addEventListener("DOMContentLoaded", () => {
window.close();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,42 @@
package agents
import (
"context"
"github.com/navidrome/navidrome/model"
)
const PlaceholderAgentName = "placeholder"
const (
placeholderArtistImageSmallUrl = "https://lastfm.freetls.fastly.net/i/u/64s/2a96cbd8b46e442fc41c2b86b821562f.png"
placeholderArtistImageMediumUrl = "https://lastfm.freetls.fastly.net/i/u/174s/2a96cbd8b46e442fc41c2b86b821562f.png"
placeholderArtistImageLargeUrl = "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png"
placeholderBiography = "Biography not available"
)
type placeholderAgent struct{}
func placeholdersConstructor(ds model.DataStore) Interface {
return &placeholderAgent{}
}
func (p *placeholderAgent) AgentName() string {
return PlaceholderAgentName
}
func (p *placeholderAgent) GetBiography(ctx context.Context, id, name, mbid string) (string, error) {
return placeholderBiography, nil
}
func (p *placeholderAgent) GetImages(ctx context.Context, id, name, mbid string) ([]ArtistImage, error) {
return []ArtistImage{
{placeholderArtistImageLargeUrl, 300},
{placeholderArtistImageMediumUrl, 174},
{placeholderArtistImageSmallUrl, 64},
}, nil
}
func init() {
Register(PlaceholderAgentName, placeholdersConstructor)
}

View File

@@ -0,0 +1,115 @@
package spotify
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/navidrome/navidrome/log"
)
const apiBaseUrl = "https://api.spotify.com/v1/"
var (
ErrNotFound = errors.New("spotify: not found")
)
type httpDoer interface {
Do(req *http.Request) (*http.Response, error)
}
func NewClient(id, secret string, hc httpDoer) *Client {
return &Client{id, secret, hc}
}
type Client struct {
id string
secret string
hc httpDoer
}
func (c *Client) SearchArtists(ctx context.Context, name string, limit int) ([]Artist, error) {
token, err := c.authorize(ctx)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("type", "artist")
params.Add("q", name)
params.Add("offset", "0")
params.Add("limit", strconv.Itoa(limit))
req, _ := http.NewRequest("GET", apiBaseUrl+"search", nil)
req.URL.RawQuery = params.Encode()
req.Header.Add("Authorization", "Bearer "+token)
var results SearchResults
err = c.makeRequest(req, &results)
if err != nil {
return nil, err
}
if len(results.Artists.Items) == 0 {
return nil, ErrNotFound
}
return results.Artists.Items, err
}
func (c *Client) authorize(ctx context.Context) (string, error) {
payload := url.Values{}
payload.Add("grant_type", "client_credentials")
encodePayload := payload.Encode()
req, _ := http.NewRequest("POST", "https://accounts.spotify.com/api/token", strings.NewReader(encodePayload))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Content-Length", strconv.Itoa(len(encodePayload)))
auth := c.id + ":" + c.secret
req.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
response := map[string]interface{}{}
err := c.makeRequest(req, &response)
if err != nil {
return "", err
}
if v, ok := response["access_token"]; ok {
return v.(string), nil
}
log.Error(ctx, "Invalid spotify response", "resp", response)
return "", errors.New("invalid response")
}
func (c *Client) makeRequest(req *http.Request, response interface{}) error {
resp, err := c.hc.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != 200 {
return c.parseError(data)
}
return json.Unmarshal(data, response)
}
func (c *Client) parseError(data []byte) error {
var e Error
err := json.Unmarshal(data, &e)
if err != nil {
return err
}
return fmt.Errorf("spotify error(%s): %s", e.Code, e.Message)
}

View File

@@ -0,0 +1,131 @@
package spotify
import (
"bytes"
"context"
"io"
"net/http"
"os"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Client", func() {
var httpClient *fakeHttpClient
var client *Client
BeforeEach(func() {
httpClient = &fakeHttpClient{}
client = NewClient("SPOTIFY_ID", "SPOTIFY_SECRET", httpClient)
})
Describe("ArtistImages", func() {
It("returns artist images from a successful request", func() {
f, _ := os.Open("tests/fixtures/spotify.search.artist.json")
httpClient.mock("https://api.spotify.com/v1/search", http.Response{Body: f, StatusCode: 200})
httpClient.mock("https://accounts.spotify.com/api/token", http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBufferString(`{"access_token": "NEW_ACCESS_TOKEN","token_type": "Bearer","expires_in": 3600}`)),
})
artists, err := client.SearchArtists(context.TODO(), "U2", 10)
Expect(err).To(BeNil())
Expect(artists).To(HaveLen(20))
Expect(artists[0].Popularity).To(Equal(82))
images := artists[0].Images
Expect(images).To(HaveLen(3))
Expect(images[0].Width).To(Equal(640))
Expect(images[1].Width).To(Equal(320))
Expect(images[2].Width).To(Equal(160))
})
It("fails if artist was not found", func() {
httpClient.mock("https://api.spotify.com/v1/search", http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBufferString(`{
"artists" : {
"href" : "https://api.spotify.com/v1/search?query=dasdasdas%2Cdna&type=artist&offset=0&limit=20",
"items" : [ ], "limit" : 20, "next" : null, "offset" : 0, "previous" : null, "total" : 0
}}`)),
})
httpClient.mock("https://accounts.spotify.com/api/token", http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBufferString(`{"access_token": "NEW_ACCESS_TOKEN","token_type": "Bearer","expires_in": 3600}`)),
})
_, err := client.SearchArtists(context.TODO(), "U2", 10)
Expect(err).To(MatchError(ErrNotFound))
})
It("fails if not able to authorize", func() {
f, _ := os.Open("tests/fixtures/spotify.search.artist.json")
httpClient.mock("https://api.spotify.com/v1/search", http.Response{Body: f, StatusCode: 200})
httpClient.mock("https://accounts.spotify.com/api/token", http.Response{
StatusCode: 400,
Body: io.NopCloser(bytes.NewBufferString(`{"error":"invalid_client","error_description":"Invalid client"}`)),
})
_, err := client.SearchArtists(context.TODO(), "U2", 10)
Expect(err).To(MatchError("spotify error(invalid_client): Invalid client"))
})
})
Describe("authorize", func() {
It("returns an access_token on successful authorization", func() {
httpClient.mock("https://accounts.spotify.com/api/token", http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBufferString(`{"access_token": "NEW_ACCESS_TOKEN","token_type": "Bearer","expires_in": 3600}`)),
})
token, err := client.authorize(context.TODO())
Expect(err).To(BeNil())
Expect(token).To(Equal("NEW_ACCESS_TOKEN"))
auth := httpClient.lastRequest.Header.Get("Authorization")
Expect(auth).To(Equal("Basic U1BPVElGWV9JRDpTUE9USUZZX1NFQ1JFVA=="))
})
It("fails on unsuccessful authorization", func() {
httpClient.mock("https://accounts.spotify.com/api/token", http.Response{
StatusCode: 400,
Body: io.NopCloser(bytes.NewBufferString(`{"error":"invalid_client","error_description":"Invalid client"}`)),
})
_, err := client.authorize(context.TODO())
Expect(err).To(MatchError("spotify error(invalid_client): Invalid client"))
})
It("fails on invalid JSON response", func() {
httpClient.mock("https://accounts.spotify.com/api/token", http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBufferString(`{NOT_VALID}`)),
})
_, err := client.authorize(context.TODO())
Expect(err).To(MatchError("invalid character 'N' looking for beginning of object key string"))
})
})
})
type fakeHttpClient struct {
responses map[string]*http.Response
lastRequest *http.Request
}
func (c *fakeHttpClient) mock(url string, response http.Response) {
if c.responses == nil {
c.responses = make(map[string]*http.Response)
}
c.responses[url] = &response
}
func (c *fakeHttpClient) Do(req *http.Request) (*http.Response, error) {
c.lastRequest = req
u := req.URL
u.RawQuery = ""
if resp, ok := c.responses[u.String()]; ok {
return resp, nil
}
panic("URL not mocked: " + u.String())
}

View File

@@ -0,0 +1,30 @@
package spotify
type SearchResults struct {
Artists ArtistsResult `json:"artists"`
}
type ArtistsResult struct {
HRef string `json:"href"`
Items []Artist `json:"items"`
}
type Artist struct {
Genres []string `json:"genres"`
HRef string `json:"href"`
ID string `json:"id"`
Popularity int `json:"popularity"`
Images []Image `json:"images"`
Name string `json:"name"`
}
type Image struct {
URL string `json:"url"`
Width int `json:"width"`
Height int `json:"height"`
}
type Error struct {
Code string `json:"error"`
Message string `json:"error_description"`
}

View File

@@ -0,0 +1,48 @@
package spotify
import (
"encoding/json"
"os"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Responses", func() {
Describe("Search type=artist", func() {
It("parses the artist search result correctly ", func() {
var resp SearchResults
body, _ := os.ReadFile("tests/fixtures/spotify.search.artist.json")
err := json.Unmarshal(body, &resp)
Expect(err).To(BeNil())
Expect(resp.Artists.Items).To(HaveLen(20))
u2 := resp.Artists.Items[0]
Expect(u2.Name).To(Equal("U2"))
Expect(u2.Genres).To(ContainElements("irish rock", "permanent wave", "rock"))
Expect(u2.ID).To(Equal("51Blml2LZPmy7TTiAg47vQ"))
Expect(u2.HRef).To(Equal("https://api.spotify.com/v1/artists/51Blml2LZPmy7TTiAg47vQ"))
Expect(u2.Images[0].URL).To(Equal("https://i.scdn.co/image/e22d5c0c8139b8439440a69854ed66efae91112d"))
Expect(u2.Images[0].Width).To(Equal(640))
Expect(u2.Images[0].Height).To(Equal(640))
Expect(u2.Images[1].URL).To(Equal("https://i.scdn.co/image/40d6c5c14355cfc127b70da221233315497ec91d"))
Expect(u2.Images[1].Width).To(Equal(320))
Expect(u2.Images[1].Height).To(Equal(320))
Expect(u2.Images[2].URL).To(Equal("https://i.scdn.co/image/7293d6752ae8a64e34adee5086858e408185b534"))
Expect(u2.Images[2].Width).To(Equal(160))
Expect(u2.Images[2].Height).To(Equal(160))
})
})
Describe("Error", func() {
It("parses the error response correctly", func() {
var errorResp Error
body := []byte(`{"error":"invalid_client","error_description":"Invalid client"}`)
err := json.Unmarshal(body, &errorResp)
Expect(err).To(BeNil())
Expect(errorResp.Code).To(Equal("invalid_client"))
Expect(errorResp.Message).To(Equal("Invalid client"))
})
})
})

View File

@@ -0,0 +1,94 @@
package spotify
import (
"context"
"fmt"
"net/http"
"sort"
"strings"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/agents"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils"
"github.com/xrash/smetrics"
)
const spotifyAgentName = "spotify"
type spotifyAgent struct {
ds model.DataStore
id string
secret string
client *Client
}
func spotifyConstructor(ds model.DataStore) agents.Interface {
l := &spotifyAgent{
ds: ds,
id: conf.Server.Spotify.ID,
secret: conf.Server.Spotify.Secret,
}
hc := &http.Client{
Timeout: consts.DefaultHttpClientTimeOut,
}
chc := utils.NewCachedHTTPClient(hc, consts.DefaultHttpClientTimeOut)
l.client = NewClient(l.id, l.secret, chc)
return l
}
func (s *spotifyAgent) AgentName() string {
return spotifyAgentName
}
func (s *spotifyAgent) GetImages(ctx context.Context, id, name, mbid string) ([]agents.ArtistImage, error) {
a, err := s.searchArtist(ctx, name)
if err != nil {
if err == model.ErrNotFound {
log.Warn(ctx, "Artist not found in Spotify", "artist", name)
} else {
log.Error(ctx, "Error calling Spotify", "artist", name, err)
}
return nil, err
}
var res []agents.ArtistImage
for _, img := range a.Images {
res = append(res, agents.ArtistImage{
URL: img.URL,
Size: img.Width,
})
}
return res, nil
}
func (s *spotifyAgent) searchArtist(ctx context.Context, name string) (*Artist, error) {
artists, err := s.client.SearchArtists(ctx, name, 40)
if err != nil || len(artists) == 0 {
return nil, model.ErrNotFound
}
name = strings.ToLower(name)
// Sort results, prioritizing artists with images, with similar names and with high popularity, in this order
sort.Slice(artists, func(i, j int) bool {
ai := fmt.Sprintf("%-5t-%03d-%04d", len(artists[i].Images) == 0, smetrics.WagnerFischer(name, strings.ToLower(artists[i].Name), 1, 1, 2), 1000-artists[i].Popularity)
aj := fmt.Sprintf("%-5t-%03d-%04d", len(artists[j].Images) == 0, smetrics.WagnerFischer(name, strings.ToLower(artists[j].Name), 1, 1, 2), 1000-artists[j].Popularity)
return ai < aj
})
// If the first one has the same name, that's the one
if strings.ToLower(artists[0].Name) != name {
return nil, model.ErrNotFound
}
return &artists[0], err
}
func init() {
conf.AddHook(func() {
if conf.Server.Spotify.ID != "" && conf.Server.Spotify.Secret != "" {
agents.Register(spotifyAgentName, spotifyConstructor)
}
})
}

View File

@@ -0,0 +1,17 @@
package spotify
import (
"testing"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestSpotify(t *testing.T) {
tests.Init(t, false)
log.SetLevel(log.LevelCritical)
RegisterFailHandler(Fail)
RunSpecs(t, "Spotify Test Suite")
}

113
core/archiver.go Normal file
View File

@@ -0,0 +1,113 @@
package core
import (
"archive/zip"
"context"
"fmt"
"io"
"os"
"path/filepath"
"github.com/Masterminds/squirrel"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
)
type Archiver interface {
ZipAlbum(ctx context.Context, id string, w io.Writer) error
ZipArtist(ctx context.Context, id string, w io.Writer) error
ZipPlaylist(ctx context.Context, id string, w io.Writer) error
}
func NewArchiver(ds model.DataStore) Archiver {
return &archiver{ds: ds}
}
type archiver struct {
ds model.DataStore
}
type createHeader func(idx int, mf model.MediaFile) *zip.FileHeader
func (a *archiver) ZipAlbum(ctx context.Context, id string, out io.Writer) error {
mfs, err := a.ds.MediaFile(ctx).GetAll(model.QueryOptions{
Filters: squirrel.Eq{"album_id": id},
Sort: "album",
})
if err != nil {
log.Error(ctx, "Error loading mediafiles from album", "id", id, err)
return err
}
return a.zipTracks(ctx, id, out, mfs, a.createHeader)
}
func (a *archiver) ZipArtist(ctx context.Context, id string, out io.Writer) error {
mfs, err := a.ds.MediaFile(ctx).GetAll(model.QueryOptions{
Sort: "album",
Filters: squirrel.Eq{"album_artist_id": id},
})
if err != nil {
log.Error(ctx, "Error loading mediafiles from artist", "id", id, err)
return err
}
return a.zipTracks(ctx, id, out, mfs, a.createHeader)
}
func (a *archiver) ZipPlaylist(ctx context.Context, id string, out io.Writer) error {
pls, err := a.ds.Playlist(ctx).Get(id)
if err != nil {
log.Error(ctx, "Error loading mediafiles from playlist", "id", id, err)
return err
}
return a.zipTracks(ctx, id, out, pls.Tracks, a.createPlaylistHeader)
}
func (a *archiver) zipTracks(ctx context.Context, id string, out io.Writer, mfs model.MediaFiles, ch createHeader) error {
z := zip.NewWriter(out)
for idx, mf := range mfs {
_ = a.addFileToZip(ctx, z, mf, ch(idx, mf))
}
err := z.Close()
if err != nil {
log.Error(ctx, "Error closing zip file", "id", id, err)
}
return err
}
func (a *archiver) createHeader(idx int, mf model.MediaFile) *zip.FileHeader {
_, file := filepath.Split(mf.Path)
return &zip.FileHeader{
Name: fmt.Sprintf("%s/%s", mf.Album, file),
Modified: mf.UpdatedAt,
Method: zip.Store,
}
}
func (a *archiver) createPlaylistHeader(idx int, mf model.MediaFile) *zip.FileHeader {
_, file := filepath.Split(mf.Path)
return &zip.FileHeader{
Name: fmt.Sprintf("%d - %s - %s", idx+1, mf.AlbumArtist, file),
Modified: mf.UpdatedAt,
Method: zip.Store,
}
}
func (a *archiver) addFileToZip(ctx context.Context, z *zip.Writer, mf model.MediaFile, zh *zip.FileHeader) error {
w, err := z.CreateHeader(zh)
if err != nil {
log.Error(ctx, "Error creating zip entry", "file", mf.Path, err)
return err
}
f, err := os.Open(mf.Path)
defer func() { _ = f.Close() }()
if err != nil {
log.Error(ctx, "Error opening file for zipping", "file", mf.Path, err)
return err
}
_, err = io.Copy(w, f)
if err != nil {
log.Error(ctx, "Error zipping file", "file", mf.Path, err)
return err
}
return nil
}

231
core/artwork.go Normal file
View File

@@ -0,0 +1,231 @@
package core
import (
"bytes"
"context"
"errors"
"fmt"
"image"
_ "image/gif"
"image/jpeg"
"image/png"
_ "image/png"
"io"
"os"
"strings"
"sync"
"time"
"github.com/dhowden/tag"
"github.com/disintegration/imaging"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/resources"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/cache"
_ "golang.org/x/image/webp"
)
type Artwork interface {
Get(ctx context.Context, id string, size int) (io.ReadCloser, error)
}
type ArtworkCache cache.FileCache
func NewArtwork(ds model.DataStore, cache ArtworkCache) Artwork {
return &artwork{ds: ds, cache: cache}
}
type artwork struct {
ds model.DataStore
cache cache.FileCache
}
type imageInfo struct {
a *artwork
id string
path string
size int
lastUpdate time.Time
}
func (ci *imageInfo) Key() string {
return fmt.Sprintf("%s.%d.%s.%d", ci.path, ci.size, ci.lastUpdate.Format(time.RFC3339Nano), conf.Server.CoverJpegQuality)
}
func (a *artwork) Get(ctx context.Context, id string, size int) (io.ReadCloser, error) {
path, lastUpdate, err := a.getImagePath(ctx, id)
if err != nil && err != model.ErrNotFound {
return nil, err
}
if !conf.Server.DevFastAccessCoverArt {
if stat, err := os.Stat(path); err == nil {
lastUpdate = stat.ModTime()
}
}
info := &imageInfo{
a: a,
id: id,
path: path,
size: size,
lastUpdate: lastUpdate,
}
r, err := a.cache.Get(ctx, info)
if err != nil {
log.Error(ctx, "Error accessing image cache", "path", path, "size", size, err)
return nil, err
}
return r, err
}
func (a *artwork) getImagePath(ctx context.Context, id string) (path string, lastUpdated time.Time, err error) {
// If id is an album cover ID
if strings.HasPrefix(id, "al-") {
log.Trace(ctx, "Looking for album art", "id", id)
id = strings.TrimPrefix(id, "al-")
var al *model.Album
al, err = a.ds.Album(ctx).Get(id)
if err != nil {
return
}
if al.CoverArtId == "" {
err = model.ErrNotFound
}
return al.CoverArtPath, al.UpdatedAt, err
}
log.Trace(ctx, "Looking for media file art", "id", id)
// Check if id is a mediaFile id
var mf *model.MediaFile
mf, err = a.ds.MediaFile(ctx).Get(id)
// If it is not, may be an albumId
if err == model.ErrNotFound {
return a.getImagePath(ctx, "al-"+id)
}
if err != nil {
return
}
// If it is a mediaFile and it has cover art, return it (if feature is disabled, skip)
if !conf.Server.DevFastAccessCoverArt && mf.HasCoverArt {
return mf.Path, mf.UpdatedAt, nil
}
// if the mediaFile does not have a coverArt, fallback to the album cover
log.Trace(ctx, "Media file does not contain art. Falling back to album art", "id", id, "albumId", "al-"+mf.AlbumID)
return a.getImagePath(ctx, "al-"+mf.AlbumID)
}
func (a *artwork) getArtwork(ctx context.Context, id string, path string, size int) (reader io.ReadCloser, err error) {
defer func() {
if err != nil {
log.Warn(ctx, "Error extracting image", "path", path, "size", size, err)
reader, err = resources.FS.Open(consts.PlaceholderAlbumArt)
if size != 0 && err == nil {
var r io.ReadCloser
r, err = resources.FS.Open(consts.PlaceholderAlbumArt)
reader, err = resizeImage(r, size, true)
}
}
}()
if path == "" {
return nil, errors.New("empty path given for artwork")
}
if size == 0 {
// If requested original size, just read from the file
if utils.IsAudioFile(path) {
reader, err = readFromTag(path)
} else {
reader, err = readFromFile(path)
}
} else {
// If requested a resized image, get the original (possibly from cache) and resize it
var r io.ReadCloser
r, err = a.Get(ctx, id, 0)
if err != nil {
return
}
defer r.Close()
reader, err = resizeImage(r, size, false)
}
return
}
func resizeImage(reader io.Reader, size int, usePng bool) (io.ReadCloser, error) {
img, _, err := image.Decode(reader)
if err != nil {
return nil, err
}
// Preserve the aspect ratio of the image.
var m *image.NRGBA
bounds := img.Bounds()
if bounds.Max.X > bounds.Max.Y {
m = imaging.Resize(img, size, 0, imaging.Lanczos)
} else {
m = imaging.Resize(img, 0, size, imaging.Lanczos)
}
buf := new(bytes.Buffer)
if usePng {
err = png.Encode(buf, m)
} else {
err = jpeg.Encode(buf, m, &jpeg.Options{Quality: conf.Server.CoverJpegQuality})
}
return io.NopCloser(buf), err
}
func readFromTag(path string) (io.ReadCloser, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
m, err := tag.ReadFrom(f)
if err != nil {
return nil, err
}
picture := m.Picture()
if picture == nil {
return nil, errors.New("file does not contain embedded art")
}
return io.NopCloser(bytes.NewReader(picture.Data)), nil
}
func readFromFile(path string) (io.ReadCloser, error) {
return os.Open(path)
}
var (
onceImageCache sync.Once
instanceImageCache ArtworkCache
)
func GetImageCache() ArtworkCache {
onceImageCache.Do(func() {
instanceImageCache = cache.NewFileCache("Image", conf.Server.ImageCacheSize, consts.ImageCacheDir, consts.DefaultImageCacheMaxItems,
func(ctx context.Context, arg cache.Item) (io.Reader, error) {
info := arg.(*imageInfo)
reader, err := info.a.getArtwork(ctx, info.id, info.path, info.size)
if err != nil {
log.Error(ctx, "Error loading artwork art", "path", info.path, "size", info.size, err)
return nil, err
}
return reader, nil
})
})
return instanceImageCache
}

141
core/artwork_test.go Normal file
View File

@@ -0,0 +1,141 @@
package core
import (
"context"
"image"
"os"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Artwork", func() {
var artwork Artwork
var ds model.DataStore
ctx := log.NewContext(context.TODO())
BeforeEach(func() {
ds = &tests.MockDataStore{MockedTranscoding: &tests.MockTranscodingRepo{}}
ds.Album(ctx).(*tests.MockAlbumRepo).SetData(model.Albums{
{ID: "222", CoverArtId: "123", CoverArtPath: "tests/fixtures/test.mp3"},
{ID: "333", CoverArtId: ""},
{ID: "444", CoverArtId: "444", CoverArtPath: "tests/fixtures/cover.jpg"},
})
ds.MediaFile(ctx).(*tests.MockMediaFileRepo).SetData(model.MediaFiles{
{ID: "123", AlbumID: "222", Path: "tests/fixtures/test.mp3", HasCoverArt: true},
{ID: "456", AlbumID: "222", Path: "tests/fixtures/test.ogg", HasCoverArt: false},
})
})
Context("Cache is configured", func() {
BeforeEach(func() {
conf.Server.DataFolder, _ = os.MkdirTemp("", "file_caches")
conf.Server.ImageCacheSize = "100MB"
cache := GetImageCache()
Eventually(func() bool { return cache.Ready(context.TODO()) }).Should(BeTrue())
artwork = NewArtwork(ds, cache)
})
It("retrieves the external artwork art for an album", func() {
r, err := artwork.Get(ctx, "al-444", 0)
Expect(err).To(BeNil())
_, format, err := image.Decode(r)
Expect(err).To(BeNil())
Expect(format).To(Equal("jpeg"))
Expect(r.Close()).To(BeNil())
})
It("retrieves the embedded artwork art for an album", func() {
r, err := artwork.Get(ctx, "al-222", 0)
Expect(err).To(BeNil())
_, format, err := image.Decode(r)
Expect(err).To(BeNil())
Expect(format).To(Equal("jpeg"))
Expect(r.Close()).To(BeNil())
})
It("returns the default artwork if album does not have artwork", func() {
r, err := artwork.Get(ctx, "al-333", 0)
Expect(err).To(BeNil())
_, format, err := image.Decode(r)
Expect(err).To(BeNil())
Expect(format).To(Equal("png"))
Expect(r.Close()).To(BeNil())
})
It("returns the default artwork if album is not found", func() {
r, err := artwork.Get(ctx, "al-0101", 0)
Expect(err).To(BeNil())
_, format, err := image.Decode(r)
Expect(err).To(BeNil())
Expect(format).To(Equal("png"))
Expect(r.Close()).To(BeNil())
})
It("retrieves the original artwork art from a media_file", func() {
r, err := artwork.Get(ctx, "123", 0)
Expect(err).To(BeNil())
img, format, err := image.Decode(r)
Expect(err).To(BeNil())
Expect(format).To(Equal("jpeg"))
Expect(img.Bounds().Size().X).To(Equal(600))
Expect(img.Bounds().Size().Y).To(Equal(600))
Expect(r.Close()).To(BeNil())
})
It("retrieves the album artwork art if media_file does not have one", func() {
r, err := artwork.Get(ctx, "456", 0)
Expect(err).To(BeNil())
_, format, err := image.Decode(r)
Expect(err).To(BeNil())
Expect(format).To(Equal("jpeg"))
Expect(r.Close()).To(BeNil())
})
It("retrieves the album artwork by album id", func() {
r, err := artwork.Get(ctx, "222", 0)
Expect(err).To(BeNil())
_, format, err := image.Decode(r)
Expect(err).To(BeNil())
Expect(format).To(Equal("jpeg"))
Expect(r.Close()).To(BeNil())
})
It("resized artwork art as requested", func() {
r, err := artwork.Get(ctx, "123", 200)
Expect(err).To(BeNil())
img, format, err := image.Decode(r)
Expect(err).To(BeNil())
Expect(format).To(Equal("jpeg"))
Expect(img.Bounds().Size().X).To(Equal(200))
Expect(img.Bounds().Size().Y).To(Equal(200))
Expect(r.Close()).To(BeNil())
})
Context("Errors", func() {
It("returns err if gets error from album table", func() {
ds.Album(ctx).(*tests.MockAlbumRepo).SetError(true)
_, err := artwork.Get(ctx, "al-222", 0)
Expect(err).To(MatchError("Error!"))
})
It("returns err if gets error from media_file table", func() {
ds.MediaFile(ctx).(*tests.MockMediaFileRepo).SetError(true)
_, err := artwork.Get(ctx, "123", 0)
Expect(err).To(MatchError("Error!"))
})
})
})
})

View File

@@ -2,75 +2,66 @@ package auth
import (
"context"
"fmt"
"sync"
"time"
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/dgrijalva/jwt-go"
"github.com/go-chi/jwtauth"
"github.com/go-chi/jwtauth/v5"
"github.com/lestrrat-go/jwx/jwt"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
)
var (
once sync.Once
JwtSecret []byte
TokenAuth *jwtauth.JWTAuth
sessionTimeOut time.Duration
once sync.Once
Secret []byte
TokenAuth *jwtauth.JWTAuth
)
func InitTokenAuth(ds model.DataStore) {
func Init(ds model.DataStore) {
once.Do(func() {
log.Info("Setting Session Timeout", "value", conf.Server.SessionTimeout)
secret, err := ds.Property(context.TODO()).DefaultGet(consts.JWTSecretKey, "not so secret")
if err != nil {
log.Error("No JWT secret found in DB. Setting a temp one, but please report this error", err)
}
JwtSecret = []byte(secret)
TokenAuth = jwtauth.New("HS256", JwtSecret, nil)
Secret = []byte(secret)
TokenAuth = jwtauth.New("HS256", Secret, nil)
})
}
func CreateToken(u *model.User) (string, error) {
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["iss"] = consts.JWTIssuer
claims["sub"] = u.UserName
claims := map[string]interface{}{}
claims[jwt.IssuerKey] = consts.JWTIssuer
claims[jwt.IssuedAtKey] = time.Now().UTC().Unix()
claims[jwt.SubjectKey] = u.UserName
claims["uid"] = u.ID
claims["adm"] = u.IsAdmin
token, _, err := TokenAuth.Encode(claims)
if err != nil {
return "", err
}
return TouchToken(token)
}
func getSessionTimeOut() time.Duration {
if sessionTimeOut == 0 {
sessionTimeOut = conf.Server.SessionTimeout
log.Info("Setting Session Timeout", "value", sessionTimeOut)
func TouchToken(token jwt.Token) (string, error) {
claims, err := token.AsMap(context.Background())
if err != nil {
return "", err
}
return sessionTimeOut
claims[jwt.ExpirationKey] = time.Now().UTC().Add(conf.Server.SessionTimeout).Unix()
_, newToken, err := TokenAuth.Encode(claims)
return newToken, err
}
func TouchToken(token *jwt.Token) (string, error) {
timeout := getSessionTimeOut()
expireIn := time.Now().Add(timeout).Unix()
claims := token.Claims.(jwt.MapClaims)
claims["exp"] = expireIn
return token.SignedString(JwtSecret)
}
func Validate(tokenStr string) (jwt.MapClaims, error) {
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
// hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
return JwtSecret, nil
})
func Validate(tokenStr string) (map[string]interface{}, error) {
token, err := jwtauth.VerifyToken(TokenAuth, tokenStr)
if err != nil {
return nil, err
}
return token.Claims.(jwt.MapClaims), err
return token.AsMap(context.Background())
}

View File

@@ -4,9 +4,12 @@ import (
"testing"
"time"
"github.com/deluan/navidrome/core/auth"
"github.com/deluan/navidrome/log"
"github.com/dgrijalva/jwt-go"
"github.com/go-chi/jwtauth/v5"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/auth"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
@@ -17,39 +20,89 @@ func TestAuth(t *testing.T) {
RunSpecs(t, "Auth Test Suite")
}
const testJWTSecret = "not so secret"
const (
testJWTSecret = "not so secret"
oneDay = 24 * time.Hour
)
var _ = Describe("Auth", func() {
BeforeEach(func() {
auth.JwtSecret = []byte(testJWTSecret)
BeforeSuite(func() {
conf.Server.SessionTimeout = 2 * oneDay
})
Context("Validate", func() {
BeforeEach(func() {
auth.Secret = []byte(testJWTSecret)
auth.TokenAuth = jwtauth.New("HS256", auth.Secret, nil)
})
Describe("Validate", func() {
It("returns error with an invalid JWT token", func() {
_, err := auth.Validate("invalid.token")
Expect(err).To(Not(BeNil()))
Expect(err).To(HaveOccurred())
})
It("returns the claims from a valid JWT token", func() {
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims := map[string]interface{}{}
claims["iss"] = "issuer"
claims["iat"] = time.Now().Unix()
claims["exp"] = time.Now().Add(1 * time.Minute).Unix()
tokenStr, _ := token.SignedString(auth.JwtSecret)
_, tokenStr, err := auth.TokenAuth.Encode(claims)
Expect(err).NotTo(HaveOccurred())
decodedClaims, err := auth.Validate(tokenStr)
Expect(err).To(BeNil())
Expect(err).NotTo(HaveOccurred())
Expect(decodedClaims["iss"]).To(Equal("issuer"))
})
It("returns ErrExpired if the `exp` field is in the past", func() {
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims := map[string]interface{}{}
claims["iss"] = "issuer"
claims["exp"] = time.Now().Add(-1 * time.Minute).Unix()
tokenStr, _ := token.SignedString(auth.JwtSecret)
_, tokenStr, err := auth.TokenAuth.Encode(claims)
Expect(err).NotTo(HaveOccurred())
_, err := auth.Validate(tokenStr)
Expect(err).To(MatchError("Token is expired"))
_, err = auth.Validate(tokenStr)
Expect(err).To(MatchError("token is expired"))
})
})
Describe("CreateToken", func() {
It("creates a valid token", func() {
u := &model.User{
ID: "123",
UserName: "johndoe",
IsAdmin: true,
}
tokenStr, err := auth.CreateToken(u)
Expect(err).NotTo(HaveOccurred())
claims, err := auth.Validate(tokenStr)
Expect(err).NotTo(HaveOccurred())
Expect(claims["iss"]).To(Equal(consts.JWTIssuer))
Expect(claims["sub"]).To(Equal("johndoe"))
Expect(claims["uid"]).To(Equal("123"))
Expect(claims["adm"]).To(Equal(true))
Expect(claims["exp"]).To(BeTemporally(">", time.Now()))
})
})
Describe("TouchToken", func() {
It("updates the expiration time", func() {
yesterday := time.Now().Add(-oneDay)
claims := map[string]interface{}{}
claims["iss"] = "issuer"
claims["exp"] = yesterday.Unix()
token, _, err := auth.TokenAuth.Encode(claims)
Expect(err).NotTo(HaveOccurred())
touched, err := auth.TouchToken(token)
Expect(err).NotTo(HaveOccurred())
decodedClaims, err := auth.Validate(touched)
Expect(err).NotTo(HaveOccurred())
exp := decodedClaims["exp"].(time.Time)
Expect(exp.Sub(yesterday)).To(BeNumerically(">=", oneDay))
})
})
})

87
core/cache_warmer.go Normal file
View File

@@ -0,0 +1,87 @@
package core
import (
"context"
"io"
"time"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/utils/pool"
)
type CacheWarmer interface {
AddAlbum(ctx context.Context, albumID string)
Flush(ctx context.Context)
}
func NewCacheWarmer(artwork Artwork, artworkCache ArtworkCache) CacheWarmer {
w := &warmer{
artwork: artwork,
artworkCache: artworkCache,
albums: map[string]struct{}{},
}
p, err := pool.NewPool("artwork", 3, w.execute)
if err != nil {
log.Error(context.Background(), "Error creating pool for Album Artwork Cache Warmer", err)
} else {
w.pool = p
}
return w
}
type warmer struct {
pool *pool.Pool
artwork Artwork
artworkCache ArtworkCache
albums map[string]struct{}
}
func (w *warmer) AddAlbum(ctx context.Context, albumID string) {
if albumID == "" {
return
}
w.albums[albumID] = struct{}{}
}
func (w *warmer) waitForCacheReady(ctx context.Context) {
for !w.artworkCache.Ready(ctx) {
time.Sleep(time.Second)
}
}
func (w *warmer) Flush(ctx context.Context) {
if conf.Server.DevPreCacheAlbumArtwork {
w.waitForCacheReady(ctx)
if w.artworkCache.Available(ctx) {
if w.pool == nil || len(w.albums) == 0 {
return
}
log.Info(ctx, "Pre-caching album artworks", "numAlbums", len(w.albums))
for id := range w.albums {
w.pool.Submit(artworkItem{albumID: id})
}
} else {
log.Warn(ctx, "Cache warmer is not available as ImageCache is DISABLED")
}
}
w.albums = map[string]struct{}{}
}
func (w *warmer) execute(workload interface{}) {
ctx := context.Background()
item := workload.(artworkItem)
log.Trace(ctx, "Pre-caching album artwork", "albumID", item.albumID)
r, err := w.artwork.Get(ctx, item.albumID, 0)
if err != nil {
log.Warn("Error pre-caching artwork from album", "id", item.albumID, err)
return
}
defer r.Close()
_, _ = io.Copy(io.Discard, r)
}
type artworkItem struct {
albumID string
}

View File

@@ -3,7 +3,7 @@ package core
import (
"context"
"github.com/deluan/navidrome/model/request"
"github.com/navidrome/navidrome/model/request"
)
func userName(ctx context.Context) string {

View File

@@ -1,35 +1,17 @@
package core
import (
"io/ioutil"
"os"
"testing"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/tests"
"github.com/djherbis/fscache"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestEngine(t *testing.T) {
func TestCore(t *testing.T) {
tests.Init(t, false)
log.SetLevel(log.LevelCritical)
RegisterFailHandler(Fail)
RunSpecs(t, "Core Suite")
}
var testCache fscache.Cache
var testCacheDir string
var _ = Describe("Core Suite Setup", func() {
BeforeSuite(func() {
testCacheDir, _ = ioutil.TempDir("", "core_test_cache")
fs, _ := fscache.NewFs(testCacheDir, 0755)
testCache, _ = fscache.NewCache(fs, nil)
})
AfterSuite(func() {
os.RemoveAll(testCacheDir)
})
})

View File

@@ -1,206 +0,0 @@
package core
import (
"bytes"
"context"
"errors"
"fmt"
"image"
_ "image/gif"
"image/jpeg"
_ "image/png"
"io"
"os"
"strings"
"time"
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/resources"
"github.com/deluan/navidrome/utils"
"github.com/dhowden/tag"
"github.com/disintegration/imaging"
"github.com/djherbis/fscache"
)
type Cover interface {
Get(ctx context.Context, id string, size int, out io.Writer) error
}
type ImageCache fscache.Cache
func NewCover(ds model.DataStore, cache ImageCache) Cover {
return &cover{ds: ds, cache: cache}
}
type cover struct {
ds model.DataStore
cache fscache.Cache
}
func (c *cover) Get(ctx context.Context, id string, size int, out io.Writer) error {
path, lastUpdate, err := c.getCoverPath(ctx, id)
if err != nil && err != model.ErrNotFound {
return err
}
// If cache is disabled, just read the coverart directly from file
if c.cache == nil {
log.Trace(ctx, "Retrieving cover art from file", "path", path, "size", size, err)
reader, err := c.getCover(ctx, path, size)
if err != nil {
log.Error(ctx, "Error loading cover art", "path", path, "size", size, err)
} else {
_, err = io.Copy(out, reader)
}
return err
}
cacheKey := imageCacheKey(path, size, lastUpdate)
r, w, err := c.cache.Get(cacheKey)
if err != nil {
log.Error(ctx, "Error reading from image cache", "path", path, "size", size, err)
return err
}
defer r.Close()
if w != nil {
log.Trace(ctx, "Image cache miss", "path", path, "size", size, "lastUpdate", lastUpdate)
go func() {
defer w.Close()
reader, err := c.getCover(ctx, path, size)
if err != nil {
log.Error(ctx, "Error loading cover art", "path", path, "size", size, err)
return
}
if _, err := io.Copy(w, reader); err != nil {
log.Error(ctx, "Error saving covert art to cache", "path", path, "size", size, err)
}
}()
} else {
log.Trace(ctx, "Loading image from cache", "path", path, "size", size, "lastUpdate", lastUpdate)
}
_, err = io.Copy(out, r)
return err
}
func (c *cover) getCoverPath(ctx context.Context, id string) (path string, lastUpdated time.Time, err error) {
// If id is an album cover ID
if strings.HasPrefix(id, "al-") {
log.Trace(ctx, "Looking for album art", "id", id)
id = strings.TrimPrefix(id, "al-")
var al *model.Album
al, err = c.ds.Album(ctx).Get(id)
if err != nil {
return
}
if al.CoverArtId == "" {
err = model.ErrNotFound
}
return al.CoverArtPath, al.UpdatedAt, err
}
log.Trace(ctx, "Looking for media file art", "id", id)
// if id is a mediafile cover id
var mf *model.MediaFile
mf, err = c.ds.MediaFile(ctx).Get(id)
if err != nil {
return
}
if mf.HasCoverArt {
return mf.Path, mf.UpdatedAt, nil
}
// if the mediafile does not have a coverArt, fallback to the album cover
log.Trace(ctx, "Media file does not contain art. Falling back to album art", "id", id, "albumId", "al-"+mf.AlbumID)
return c.getCoverPath(ctx, "al-"+mf.AlbumID)
}
func imageCacheKey(path string, size int, lastUpdate time.Time) string {
return fmt.Sprintf("%s.%d.%s.%d", path, size, lastUpdate.Format(time.RFC3339Nano), conf.Server.CoverJpegQuality)
}
func (c *cover) getCover(ctx context.Context, path string, size int) (reader io.Reader, err error) {
defer func() {
if err != nil {
log.Warn(ctx, "Error extracting image", "path", path, "size", size, err)
reader, err = resources.AssetFile().Open(consts.PlaceholderAlbumArt)
}
}()
if path == "" {
return nil, errors.New("empty path given for cover")
}
var data []byte
if utils.IsAudioFile(path) {
data, err = readFromTag(path)
} else {
data, err = readFromFile(path)
}
if err != nil {
return
} else if size > 0 {
data, err = resizeImage(bytes.NewReader(data), size)
}
// Confirm the image is valid. Costly, but necessary
_, _, err = image.Decode(bytes.NewReader(data))
if err == nil {
reader = bytes.NewReader(data)
}
return
}
func resizeImage(reader io.Reader, size int) ([]byte, error) {
img, _, err := image.Decode(reader)
if err != nil {
return nil, err
}
m := imaging.Resize(img, size, size, imaging.Lanczos)
buf := new(bytes.Buffer)
err = jpeg.Encode(buf, m, &jpeg.Options{Quality: conf.Server.CoverJpegQuality})
return buf.Bytes(), err
}
func readFromTag(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
m, err := tag.ReadFrom(f)
if err != nil {
return nil, err
}
picture := m.Picture()
if picture == nil {
return nil, errors.New("file does not contain embedded art")
}
return picture.Data, nil
}
func readFromFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var buf bytes.Buffer
if _, err := buf.ReadFrom(f); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func NewImageCache() (ImageCache, error) {
return newFileCache("Image", conf.Server.ImageCacheSize, consts.ImageCacheDir, consts.DefaultImageCacheMaxItems)
}

View File

@@ -1,136 +0,0 @@
package core
import (
"bytes"
"context"
"image"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/persistence"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Cover", func() {
var cover Cover
var ds model.DataStore
ctx := log.NewContext(context.TODO())
BeforeEach(func() {
ds = &persistence.MockDataStore{MockedTranscoding: &mockTranscodingRepository{}}
ds.Album(ctx).(*persistence.MockAlbum).SetData(`[{"id": "222", "coverArtId": "123", "coverArtPath":"tests/fixtures/test.mp3"}, {"id": "333", "coverArtId": ""}, {"id": "444", "coverArtId": "444", "coverArtPath": "tests/fixtures/cover.jpg"}]`)
ds.MediaFile(ctx).(*persistence.MockMediaFile).SetData(`[{"id": "123", "albumId": "222", "path": "tests/fixtures/test.mp3", "hasCoverArt": true, "updatedAt":"2020-04-02T21:29:31.6377Z"},{"id": "456", "albumId": "222", "path": "tests/fixtures/test.ogg", "hasCoverArt": false, "updatedAt":"2020-04-02T21:29:31.6377Z"}]`)
})
Context("Cache is configured", func() {
BeforeEach(func() {
cover = NewCover(ds, testCache)
})
It("retrieves the external cover art for an album", func() {
buf := new(bytes.Buffer)
Expect(cover.Get(ctx, "al-444", 0, buf)).To(BeNil())
_, format, err := image.Decode(bytes.NewReader(buf.Bytes()))
Expect(err).To(BeNil())
Expect(format).To(Equal("jpeg"))
})
It("retrieves the embedded cover art for an album", func() {
buf := new(bytes.Buffer)
Expect(cover.Get(ctx, "al-222", 0, buf)).To(BeNil())
_, format, err := image.Decode(bytes.NewReader(buf.Bytes()))
Expect(err).To(BeNil())
Expect(format).To(Equal("jpeg"))
})
It("returns the default cover if album does not have cover", func() {
buf := new(bytes.Buffer)
Expect(cover.Get(ctx, "al-333", 0, buf)).To(BeNil())
_, format, err := image.Decode(bytes.NewReader(buf.Bytes()))
Expect(err).To(BeNil())
Expect(format).To(Equal("png"))
})
It("returns the default cover if album is not found", func() {
buf := new(bytes.Buffer)
Expect(cover.Get(ctx, "al-0101", 0, buf)).To(BeNil())
_, format, err := image.Decode(bytes.NewReader(buf.Bytes()))
Expect(err).To(BeNil())
Expect(format).To(Equal("png"))
})
It("retrieves the original cover art from a media_file", func() {
buf := new(bytes.Buffer)
Expect(cover.Get(ctx, "123", 0, buf)).To(BeNil())
img, format, err := image.Decode(bytes.NewReader(buf.Bytes()))
Expect(err).To(BeNil())
Expect(format).To(Equal("jpeg"))
Expect(img.Bounds().Size().X).To(Equal(600))
Expect(img.Bounds().Size().Y).To(Equal(600))
})
It("retrieves the album cover art if media_file does not have one", func() {
buf := new(bytes.Buffer)
Expect(cover.Get(ctx, "456", 0, buf)).To(BeNil())
_, format, err := image.Decode(bytes.NewReader(buf.Bytes()))
Expect(err).To(BeNil())
Expect(format).To(Equal("jpeg"))
})
It("resized cover art as requested", func() {
buf := new(bytes.Buffer)
Expect(cover.Get(ctx, "123", 200, buf)).To(BeNil())
img, format, err := image.Decode(bytes.NewReader(buf.Bytes()))
Expect(err).To(BeNil())
Expect(format).To(Equal("jpeg"))
Expect(img.Bounds().Size().X).To(Equal(200))
Expect(img.Bounds().Size().Y).To(Equal(200))
})
Context("Errors", func() {
It("returns err if gets error from album table", func() {
ds.Album(ctx).(*persistence.MockAlbum).SetError(true)
buf := new(bytes.Buffer)
Expect(cover.Get(ctx, "al-222", 0, buf)).To(MatchError("Error!"))
})
It("returns err if gets error from media_file table", func() {
ds.MediaFile(ctx).(*persistence.MockMediaFile).SetError(true)
buf := new(bytes.Buffer)
Expect(cover.Get(ctx, "123", 0, buf)).To(MatchError("Error!"))
})
})
})
Context("Cache is NOT configured", func() {
BeforeEach(func() {
cover = NewCover(ds, nil)
})
It("retrieves the original cover art from an album", func() {
buf := new(bytes.Buffer)
Expect(cover.Get(ctx, "al-222", 0, buf)).To(BeNil())
_, format, err := image.Decode(bytes.NewReader(buf.Bytes()))
Expect(err).To(BeNil())
Expect(format).To(Equal("jpeg"))
})
})
})

395
core/external_metadata.go Normal file
View File

@@ -0,0 +1,395 @@
package core
import (
"context"
"sort"
"strings"
"sync"
"time"
"github.com/Masterminds/squirrel"
"github.com/microcosm-cc/bluemonday"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/agents"
_ "github.com/navidrome/navidrome/core/agents/lastfm"
_ "github.com/navidrome/navidrome/core/agents/spotify"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils"
)
const (
unavailableArtistID = "-1"
maxSimilarArtists = 100
)
type ExternalMetadata interface {
UpdateArtistInfo(ctx context.Context, id string, count int, includeNotPresent bool) (*model.Artist, error)
SimilarSongs(ctx context.Context, id string, count int) (model.MediaFiles, error)
TopSongs(ctx context.Context, artist string, count int) (model.MediaFiles, error)
}
type externalMetadata struct {
ds model.DataStore
ag *agents.Agents
}
type auxArtist struct {
model.Artist
Name string
}
func NewExternalMetadata(ds model.DataStore, agents *agents.Agents) ExternalMetadata {
return &externalMetadata{ds: ds, ag: agents}
}
func (e *externalMetadata) getArtist(ctx context.Context, id string) (*auxArtist, error) {
var entity interface{}
entity, err := GetEntityByID(ctx, e.ds, id)
if err != nil {
return nil, err
}
var artist auxArtist
switch v := entity.(type) {
case *model.Artist:
artist.Artist = *v
artist.Name = clearName(v.Name)
case *model.MediaFile:
return e.getArtist(ctx, v.ArtistID)
case *model.Album:
return e.getArtist(ctx, v.AlbumArtistID)
default:
return nil, model.ErrNotFound
}
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.getArtist(ctx, id)
if err != nil {
return nil, err
}
// If we have fresh info, just return it and trigger a refresh in the background
if time.Since(artist.ExternalInfoUpdatedAt) < consts.ArtistInfoTimeToLive {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
err := e.refreshArtistInfo(ctx, artist)
if err != nil {
log.Error("Error refreshing ArtistInfo", "id", id, "name", artist.Name, err)
}
}()
log.Debug("Found cached ArtistInfo, refreshing in the background", "updatedAt", artist.ExternalInfoUpdatedAt, "name", artist.Name)
err := e.loadSimilar(ctx, artist, similarCount, includeNotPresent)
return &artist.Artist, err
}
log.Debug(ctx, "ArtistInfo not cached or expired", "updatedAt", artist.ExternalInfoUpdatedAt, "id", id, "name", artist.Name)
err = e.refreshArtistInfo(ctx, artist)
if err != nil {
return nil, err
}
err = e.loadSimilar(ctx, artist, similarCount, includeNotPresent)
return &artist.Artist, err
}
func (e *externalMetadata) refreshArtistInfo(ctx context.Context, artist *auxArtist) error {
// Get MBID first, if it is not yet available
if artist.MbzArtistID == "" {
mbid, err := e.ag.GetMBID(ctx, artist.ID, artist.Name)
if mbid != "" && err == nil {
artist.MbzArtistID = mbid
}
}
// Call all registered agents and collect information
callParallel([]func(){
func() { e.callGetBiography(ctx, e.ag, artist) },
func() { e.callGetURL(ctx, e.ag, artist) },
func() { e.callGetImage(ctx, e.ag, artist) },
func() { e.callGetSimilar(ctx, e.ag, artist, maxSimilarArtists, true) },
})
if utils.IsCtxDone(ctx) {
log.Warn(ctx, "ArtistInfo update canceled", ctx.Err())
return ctx.Err()
}
artist.ExternalInfoUpdatedAt = 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, err)
}
log.Trace(ctx, "ArtistInfo collected", "artist", artist)
return nil
}
func callParallel(fs []func()) {
wg := &sync.WaitGroup{}
wg.Add(len(fs))
for _, f := range fs {
go func(f func()) {
f()
wg.Done()
}(f)
}
wg.Wait()
}
func (e *externalMetadata) SimilarSongs(ctx context.Context, id string, count int) (model.MediaFiles, error) {
artist, err := e.getArtist(ctx, id)
if err != nil {
return nil, err
}
e.callGetSimilar(ctx, e.ag, artist, 15, false)
if utils.IsCtxDone(ctx) {
log.Warn(ctx, "SimilarSongs call canceled", ctx.Err())
return nil, ctx.Err()
}
artists := model.Artists{artist.Artist}
artists = append(artists, artist.SimilarArtists...)
weightedSongs := utils.NewWeightedRandomChooser()
for _, a := range artists {
if utils.IsCtxDone(ctx) {
log.Warn(ctx, "SimilarSongs call canceled", ctx.Err())
return nil, ctx.Err()
}
topCount := utils.MaxInt(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)
continue
}
weight := topCount * 4
for _, mf := range topSongs {
weightedSongs.Put(mf, weight)
weight -= 4
}
}
var similarSongs model.MediaFiles
for len(similarSongs) < count && weightedSongs.Size() > 0 {
s, err := weightedSongs.GetAndRemove()
if err != nil {
log.Warn(ctx, "Error getting weighted song", err)
continue
}
similarSongs = append(similarSongs, s.(model.MediaFile))
}
return similarSongs, nil
}
func (e *externalMetadata) TopSongs(ctx context.Context, artistName string, count int) (model.MediaFiles, error) {
artist, err := e.findArtistByName(ctx, artistName)
if err != nil {
log.Error(ctx, "Artist not found", "name", artistName, err)
return nil, nil
}
return e.getMatchingTopSongs(ctx, e.ag, artist, count)
}
func (e *externalMetadata) getMatchingTopSongs(ctx context.Context, agent agents.ArtistTopSongsRetriever, artist *auxArtist, count int) (model.MediaFiles, error) {
songs, err := agent.GetTopSongs(ctx, artist.ID, artist.Name, artist.MbzArtistID, count)
if err != nil {
return nil, err
}
var mfs model.MediaFiles
for _, t := range songs {
mf, err := e.findMatchingTrack(ctx, t.MBID, artist.ID, t.Name)
if err != nil {
continue
}
mfs = append(mfs, *mf)
if len(mfs) == count {
break
}
}
if len(mfs) == 0 {
log.Debug(ctx, "No matching top songs found", "name", artist.Name)
} else {
log.Debug(ctx, "Found matching top songs", "name", artist.Name, "numSongs", len(mfs))
}
return mfs, nil
}
func (e *externalMetadata) findMatchingTrack(ctx context.Context, mbid string, artistID, title string) (*model.MediaFile, error) {
if mbid != "" {
mfs, err := e.ds.MediaFile(ctx).GetAll(model.QueryOptions{
Filters: squirrel.Eq{"mbz_track_id": mbid},
})
if err == nil && len(mfs) > 0 {
return &mfs[0], nil
}
}
mfs, err := e.ds.MediaFile(ctx).GetAll(model.QueryOptions{
Filters: squirrel.And{
squirrel.Or{
squirrel.Eq{"artist_id": artistID},
squirrel.Eq{"album_artist_id": artistID},
},
squirrel.Like{"title": title},
},
Sort: "starred desc, rating desc, year asc",
})
if err != nil || len(mfs) == 0 {
return nil, model.ErrNotFound
}
return &mfs[0], nil
}
func (e *externalMetadata) callGetURL(ctx context.Context, agent agents.ArtistURLRetriever, artist *auxArtist) {
url, err := agent.GetURL(ctx, artist.ID, artist.Name, artist.MbzArtistID)
if url == "" || err != nil {
return
}
artist.ExternalUrl = url
}
func (e *externalMetadata) callGetBiography(ctx context.Context, agent agents.ArtistBiographyRetriever, artist *auxArtist) {
bio, err := agent.GetBiography(ctx, artist.ID, clearName(artist.Name), artist.MbzArtistID)
if bio == "" || err != nil {
return
}
policy := bluemonday.UGCPolicy()
bio = policy.Sanitize(bio)
bio = strings.ReplaceAll(bio, "\n", " ")
artist.Biography = strings.ReplaceAll(bio, "<a ", "<a target='_blank' ")
}
func (e *externalMetadata) callGetImage(ctx context.Context, agent agents.ArtistImageRetriever, artist *auxArtist) {
images, err := agent.GetImages(ctx, artist.ID, artist.Name, artist.MbzArtistID)
if len(images) == 0 || err != nil {
return
}
sort.Slice(images, func(i, j int) bool { return images[i].Size > images[j].Size })
if len(images) >= 1 {
artist.LargeImageUrl = images[0].URL
}
if len(images) >= 2 {
artist.MediumImageUrl = images[1].URL
}
if len(images) >= 3 {
artist.SmallImageUrl = images[2].URL
}
}
func (e *externalMetadata) callGetSimilar(ctx context.Context, agent agents.ArtistSimilarRetriever, artist *auxArtist,
limit int, includeNotPresent bool) {
similar, err := agent.GetSimilar(ctx, artist.ID, artist.Name, artist.MbzArtistID, limit)
if len(similar) == 0 || err != nil {
return
}
sa, err := e.mapSimilarArtists(ctx, similar, includeNotPresent)
if err != nil {
return
}
artist.SimilarArtists = sa
}
func (e *externalMetadata) mapSimilarArtists(ctx context.Context, similar []agents.Artist, includeNotPresent bool) (model.Artists, error) {
var result model.Artists
var notPresent []string
// First select artists that are present.
for _, s := range similar {
sa, err := e.findArtistByName(ctx, s.Name)
if err != nil {
notPresent = append(notPresent, s.Name)
continue
}
result = append(result, sa.Artist)
}
// Then fill up with non-present artists
if includeNotPresent {
for _, s := range notPresent {
sa := model.Artist{ID: unavailableArtistID, Name: s}
result = append(result, sa)
}
}
return result, nil
}
func (e *externalMetadata) findArtistByName(ctx context.Context, artistName string) (*auxArtist, error) {
artists, err := e.ds.Artist(ctx).GetAll(model.QueryOptions{
Filters: squirrel.Like{"artist.name": artistName},
Max: 1,
})
if err != nil {
return nil, err
}
if len(artists) == 0 {
return nil, model.ErrNotFound
}
artist := &auxArtist{
Artist: artists[0],
Name: clearName(artists[0].Name),
}
return artist, nil
}
func (e *externalMetadata) loadSimilar(ctx context.Context, artist *auxArtist, count int, includeNotPresent bool) error {
var ids []string
for _, sa := range artist.SimilarArtists {
if sa.ID == unavailableArtistID {
continue
}
ids = append(ids, sa.ID)
}
similar, err := e.ds.Artist(ctx).GetAll(model.QueryOptions{
Filters: squirrel.Eq{"artist.id": ids},
})
if err != nil {
log.Error("Error loading similar artists", "id", artist.ID, "name", artist.Name, err)
return err
}
// Use a map and iterate through original array, to keep the same order
artistMap := make(map[string]model.Artist)
for _, sa := range similar {
artistMap[sa.ID] = sa
}
var loaded model.Artists
for _, sa := range artist.SimilarArtists {
if len(loaded) >= count {
break
}
la, ok := artistMap[sa.ID]
if !ok {
if !includeNotPresent {
continue
}
la = sa
la.ID = unavailableArtistID
}
loaded = append(loaded, la)
}
artist.SimilarArtists = loaded
return nil
}

View File

@@ -1,32 +0,0 @@
package core
import (
"fmt"
"path/filepath"
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/log"
"github.com/djherbis/fscache"
"github.com/dustin/go-humanize"
)
func newFileCache(name, cacheSize, cacheFolder string, maxItems int) (fscache.Cache, error) {
if cacheSize == "0" {
log.Warn(fmt.Sprintf("%s cache disabled", name))
return nil, nil
}
size, err := humanize.ParseBytes(cacheSize)
if err != nil {
size = consts.DefaultCacheSize
}
lru := fscache.NewLRUHaunter(maxItems, int64(size), consts.DefaultCacheCleanUpInterval)
h := fscache.NewLRUHaunterStrategy(lru)
cacheFolder = filepath.Join(conf.Server.DataFolder, cacheFolder)
log.Info(fmt.Sprintf("Creating %s cache", name), "path", cacheFolder, "maxSize", humanize.Bytes(size))
fs, err := fscache.NewFs(cacheFolder, 0755)
if err != nil {
return nil, err
}
return fscache.NewCacheWithHaunter(fs, h)
}

View File

@@ -1,37 +0,0 @@
package core
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/deluan/navidrome/conf"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("File Caches", func() {
BeforeEach(func() {
conf.Server.DataFolder, _ = ioutil.TempDir("", "file_caches")
})
AfterEach(func() {
os.RemoveAll(conf.Server.DataFolder)
})
Describe("newFileCache", func() {
It("creates the cache folder", func() {
Expect(newFileCache("test", "1k", "test", 10)).ToNot(BeNil())
_, err := os.Stat(filepath.Join(conf.Server.DataFolder, "test"))
Expect(os.IsNotExist(err)).To(BeFalse())
})
It("creates the cache folder with invalid size", func() {
Expect(newFileCache("test", "abc", "test", 10)).ToNot(BeNil())
})
It("returns empty if cache size is '0'", func() {
Expect(newFileCache("test", "0", "test", 10)).To(BeNil())
})
})
})

28
core/get_entity.go Normal file
View File

@@ -0,0 +1,28 @@
package core
import (
"context"
"github.com/navidrome/navidrome/model"
)
// TODO: Should the type be encoded in the ID?
func GetEntityByID(ctx context.Context, ds model.DataStore, id string) (interface{}, error) {
ar, err := ds.Artist(ctx).Get(id)
if err == nil {
return ar, nil
}
al, err := ds.Album(ctx).Get(id)
if err == nil {
return al, nil
}
pls, err := ds.Playlist(ctx).Get(id)
if err == nil {
return pls, nil
}
mf, err := ds.MediaFile(ctx).Get(id)
if err == nil {
return mf, nil
}
return nil, err
}

View File

@@ -6,31 +6,43 @@ import (
"io"
"mime"
"os"
"sync"
"time"
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/core/transcoder"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/model/request"
"github.com/djherbis/fscache"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/transcoder"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/utils/cache"
)
type MediaStreamer interface {
NewStream(ctx context.Context, id string, reqFormat string, reqBitRate int) (*Stream, error)
}
type TranscodingCache fscache.Cache
type TranscodingCache cache.FileCache
func NewMediaStreamer(ds model.DataStore, ffm transcoder.Transcoder, cache TranscodingCache) MediaStreamer {
return &mediaStreamer{ds: ds, ffm: ffm, cache: cache}
func NewMediaStreamer(ds model.DataStore, t transcoder.Transcoder, cache TranscodingCache) MediaStreamer {
return &mediaStreamer{ds: ds, transcoder: t, cache: cache}
}
type mediaStreamer struct {
ds model.DataStore
ffm transcoder.Transcoder
cache fscache.Cache
ds model.DataStore
transcoder transcoder.Transcoder
cache cache.FileCache
}
type streamJob struct {
ms *mediaStreamer
mf *model.MediaFile
format string
bitRate 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)
}
func (ms *mediaStreamer) NewStream(ctx context.Context, id string, reqFormat string, reqBitRate int) (*Stream, error) {
@@ -49,92 +61,45 @@ func (ms *mediaStreamer) NewStream(ctx context.Context, id string, reqFormat str
}()
format, bitRate = selectTranscodingOptions(ctx, ms.ds, mf, reqFormat, reqBitRate)
log.Trace(ctx, "Selected transcoding options",
"requestBitrate", reqBitRate, "requestFormat", reqFormat,
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix,
"selectedBitrate", bitRate, "selectedFormat", format,
)
s := &Stream{ctx: ctx, mf: mf, format: format, bitRate: bitRate}
if format == "raw" {
log.Debug(ctx, "Streaming raw file", "id", mf.ID, "path", mf.Path,
log.Debug(ctx, "Streaming RAW file", "id", mf.ID, "path", mf.Path,
"requestBitrate", reqBitRate, "requestFormat", reqFormat,
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix)
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix,
"selectedBitrate", bitRate, "selectedFormat", format)
f, err := os.Open(mf.Path)
if err != nil {
return nil, err
}
s.Reader = f
s.Closer = f
s.ReadCloser = f
s.Seeker = f
s.format = mf.Suffix
return s, nil
}
key := cacheKey(id, bitRate, format)
r, w, err := ms.cache.Get(key)
job := &streamJob{
ms: ms,
mf: mf,
format: format,
bitRate: bitRate,
}
r, err := ms.cache.Get(ctx, job)
if err != nil {
log.Error(ctx, "Error creating stream caching buffer", "id", mf.ID, err)
log.Error(ctx, "Error accessing transcoding cache", "id", mf.ID, err)
return nil, err
}
cached = r.Cached
cached = w == nil
s.ReadCloser = r
s.Seeker = r.Seeker
// If this is a brand new transcoding request, not in the cache, start transcoding
if !cached {
log.Trace(ctx, "Cache miss. Starting new transcoding session", "id", mf.ID)
t, err := ms.ds.Transcoding(ctx).FindByFormat(format)
if err != nil {
log.Error(ctx, "Error loading transcoding command", "format", format, err)
return nil, os.ErrInvalid
}
out, err := ms.ffm.Start(ctx, t.Command, mf.Path, bitRate)
if err != nil {
log.Error(ctx, "Error starting transcoder", "id", mf.ID, err)
return nil, os.ErrInvalid
}
go copyAndClose(ctx, w, out)
}
// If it is in the cache, check if the stream is done being written. If so, return a ReaderSeeker
if cached {
size := getFinalCachedSize(r)
if size > 0 {
log.Debug(ctx, "Streaming cached file", "id", mf.ID, "path", mf.Path,
"requestBitrate", reqBitRate, "requestFormat", reqFormat,
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix, "size", size)
sr := io.NewSectionReader(r, 0, size)
s.Reader = sr
s.Closer = r
s.Seeker = sr
s.format = format
return s, nil
}
}
log.Debug(ctx, "Streaming transcoded file", "id", mf.ID, "path", mf.Path,
log.Debug(ctx, "Streaming TRANSCODED file", "id", mf.ID, "path", mf.Path,
"requestBitrate", reqBitRate, "requestFormat", reqFormat,
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix)
// All other cases, just return a ReadCloser, without Seek capabilities
s.Reader = r
s.Closer = r
s.format = format
return s, nil
}
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix,
"selectedBitrate", bitRate, "selectedFormat", format, "cached", cached, "seekable", s.Seekable())
func copyAndClose(ctx context.Context, w io.WriteCloser, r io.ReadCloser) {
_, err := io.Copy(w, r)
if err != nil {
log.Error(ctx, "Error copying data to cache", err)
}
err = r.Close()
if err != nil {
log.Error(ctx, "Error closing transcode output", err)
}
err = w.Close()
if err != nil {
log.Error(ctx, "Error closing cache", err)
}
return s, nil
}
type Stream struct {
@@ -142,8 +107,7 @@ type Stream struct {
mf *model.MediaFile
bitRate int
format string
io.Reader
io.Closer
io.ReadCloser
io.Seeker
}
@@ -202,21 +166,29 @@ func selectTranscodingOptions(ctx context.Context, ds model.DataStore, mf *model
return
}
func cacheKey(id string, bitRate int, format string) string {
return fmt.Sprintf("%s.%d.%s", id, bitRate, format)
}
var (
onceTranscodingCache sync.Once
instanceTranscodingCache TranscodingCache
)
func getFinalCachedSize(r fscache.ReadAtCloser) int64 {
cr, ok := r.(*fscache.CacheReader)
if ok {
size, final, err := cr.Size()
if final && err == nil {
return size
}
}
return -1
}
func NewTranscodingCache() (TranscodingCache, error) {
return newFileCache("Transcoding", conf.Server.TranscodingCacheSize, consts.TranscodingCacheDir, consts.DefaultTranscodingCacheMaxItems)
func GetTranscodingCache() TranscodingCache {
onceTranscodingCache.Do(func() {
instanceTranscodingCache = cache.NewFileCache("Transcoding", conf.Server.TranscodingCacheSize,
consts.TranscodingCacheDir, consts.DefaultTranscodingCacheMaxItems,
func(ctx context.Context, arg cache.Item) (io.Reader, error) {
job := arg.(*streamJob)
t, err := job.ms.ds.Transcoding(ctx).FindByFormat(job.format)
if err != nil {
log.Error(ctx, "Error loading transcoding command", "format", job.format, err)
return nil, os.ErrInvalid
}
out, err := job.ms.transcoder.Start(ctx, t.Command, job.mf.Path, job.bitRate)
if err != nil {
log.Error(ctx, "Error starting transcoder", "id", job.mf.ID, err)
return nil, os.ErrInvalid
}
return out, nil
})
})
return instanceTranscodingCache
}

View File

@@ -3,12 +3,14 @@ package core
import (
"context"
"io"
"os"
"strings"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/model/request"
"github.com/deluan/navidrome/persistence"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
@@ -20,8 +22,14 @@ var _ = Describe("MediaStreamer", func() {
ctx := log.NewContext(context.TODO())
BeforeEach(func() {
ds = &persistence.MockDataStore{MockedTranscoding: &mockTranscodingRepository{}}
ds.MediaFile(ctx).(*persistence.MockMediaFile).SetData(`[{"id": "123", "path": "tests/fixtures/test.mp3", "suffix": "mp3", "bitRate": 128, "duration": 257.0}]`)
conf.Server.DataFolder, _ = os.MkdirTemp("", "file_caches")
conf.Server.TranscodingCacheSize = "100MB"
ds = &tests.MockDataStore{MockedTranscoding: &tests.MockTranscodingRepo{}}
ds.MediaFile(ctx).(*tests.MockMediaFileRepo).SetData(model.MediaFiles{
{ID: "123", Path: "tests/fixtures/test.mp3", Suffix: "mp3", BitRate: 128, Duration: 257.0},
})
testCache := GetTranscodingCache()
Eventually(func() bool { return testCache.Ready(context.TODO()) }).Should(BeTrue())
streamer = NewMediaStreamer(ds, ffmpeg, testCache)
})
@@ -48,8 +56,13 @@ var _ = Describe("MediaStreamer", func() {
Expect(s.Duration()).To(Equal(float32(257.0)))
})
It("returns a seekable stream if the file is complete in the cache", func() {
Eventually(func() bool { return ffmpeg.closed }).Should(BeTrue())
s, err := streamer.NewStream(ctx, "123", "mp3", 64)
s, err := streamer.NewStream(ctx, "123", "mp3", 32)
Expect(err).To(BeNil())
_, _ = io.ReadAll(s)
_ = s.Close()
Eventually(func() bool { return ffmpeg.closed }, "3s").Should(BeTrue())
s, err = streamer.NewStream(ctx, "123", "mp3", 32)
Expect(err).To(BeNil())
Expect(s.Seekable()).To(BeTrue())
})

View File

@@ -1,14 +1,14 @@
package engine
package core
import (
"context"
"fmt"
"time"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/model/request"
"github.com/google/uuid"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
)
type Players interface {
@@ -24,7 +24,7 @@ type players struct {
ds model.DataStore
}
func (p *players) Register(ctx context.Context, id, client, typ, ip string) (*model.Player, *model.Transcoding, error) {
func (p *players) Register(ctx context.Context, id, client, userAgent, ip string) (*model.Player, *model.Transcoding, error) {
var plr *model.Player
var trc *model.Transcoding
var err error
@@ -36,23 +36,23 @@ func (p *players) Register(ctx context.Context, id, client, typ, ip string) (*mo
}
}
if err != nil || id == "" {
plr, err = p.ds.Player(ctx).FindByName(client, userName)
plr, err = p.ds.Player(ctx).FindMatch(userName, client, userAgent)
if err == nil {
log.Debug("Found player by name", "id", plr.ID, "client", client, "username", userName)
log.Debug("Found matching player", "id", plr.ID, "client", client, "username", userName, "type", userAgent)
} else {
r, _ := uuid.NewRandom()
plr = &model.Player{
ID: r.String(),
Name: fmt.Sprintf("%s (%s)", client, userName),
UserName: userName,
Client: client,
ID: uuid.NewString(),
UserName: userName,
Client: client,
ScrobbleEnabled: true,
}
log.Info("Registering new player", "id", plr.ID, "client", client, "username", userName)
log.Info("Registering new player", "id", plr.ID, "client", client, "username", userName, "type", userAgent)
}
}
plr.LastSeen = time.Now()
plr.Type = typ
plr.Name = fmt.Sprintf("%s [%s]", client, userAgent)
plr.UserAgent = userAgent
plr.IPAddress = ip
plr.LastSeen = time.Now()
err = p.ds.Player(ctx).Put(plr)
if err != nil {
return nil, nil, err

View File

@@ -1,13 +1,13 @@
package engine
package core
import (
"context"
"time"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/model/request"
"github.com/deluan/navidrome/persistence"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
@@ -22,7 +22,7 @@ var _ = Describe("Players", func() {
BeforeEach(func() {
repo = &mockPlayerRepository{}
ds := &persistence.MockDataStore{MockedPlayer: repo, MockedTranscoding: &mockTranscodingRepository{}}
ds := &tests.MockDataStore{MockedPlayer: repo, MockedTranscoding: &tests.MockTranscodingRepo{}}
players = NewPlayers(ds)
beforeRegister = time.Now()
})
@@ -35,7 +35,7 @@ var _ = Describe("Players", func() {
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
Expect(p.Client).To(Equal("client"))
Expect(p.UserName).To(Equal("johndoe"))
Expect(p.Type).To(Equal("chrome"))
Expect(p.UserAgent).To(Equal("chrome"))
Expect(repo.lastSaved).To(Equal(p))
Expect(trc).To(BeNil())
})
@@ -125,7 +125,7 @@ func (m *mockPlayerRepository) Get(id string) (*model.Player, error) {
return nil, model.ErrNotFound
}
func (m *mockPlayerRepository) FindByName(client, userName string) (*model.Player, error) {
func (m *mockPlayerRepository) FindMatch(userName, client, typ string) (*model.Player, error) {
for _, p := range m.data {
if p.Client == client && p.UserName == userName {
return &p, nil

View File

@@ -0,0 +1,115 @@
package scrobbler
import (
"context"
"time"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
)
func newBufferedScrobbler(ds model.DataStore, s Scrobbler, service string) *bufferedScrobbler {
b := &bufferedScrobbler{ds: ds, wrapped: s, service: service}
b.wakeSignal = make(chan struct{}, 1)
go b.run()
return b
}
type bufferedScrobbler struct {
ds model.DataStore
wrapped Scrobbler
service string
wakeSignal chan struct{}
}
func (b *bufferedScrobbler) IsAuthorized(ctx context.Context, userId string) bool {
return b.wrapped.IsAuthorized(ctx, userId)
}
func (b *bufferedScrobbler) NowPlaying(ctx context.Context, userId string, track *model.MediaFile) error {
return b.wrapped.NowPlaying(ctx, userId, track)
}
func (b *bufferedScrobbler) Scrobble(ctx context.Context, userId string, s Scrobble) error {
err := b.ds.ScrobbleBuffer(ctx).Enqueue(b.service, userId, s.ID, s.TimeStamp)
if err != nil {
return err
}
b.sendWakeSignal()
return nil
}
func (b *bufferedScrobbler) sendWakeSignal() {
// Don't block if the previous signal was not read yet
select {
case b.wakeSignal <- struct{}{}:
default:
}
}
func (b *bufferedScrobbler) run() {
ctx := context.Background()
for {
if !b.processQueue(ctx) {
time.AfterFunc(5*time.Second, func() {
b.sendWakeSignal()
})
}
<-b.wakeSignal
}
}
func (b *bufferedScrobbler) processQueue(ctx context.Context) bool {
buffer := b.ds.ScrobbleBuffer(ctx)
userIds, err := buffer.UserIDs(b.service)
if err != nil {
log.Error(ctx, "Error retrieving userIds from scrobble buffer", "scrobbler", b.service, err)
return false
}
result := true
for _, userId := range userIds {
if !b.processUserQueue(ctx, userId) {
result = false
}
}
return result
}
func (b *bufferedScrobbler) processUserQueue(ctx context.Context, userId string) bool {
buffer := b.ds.ScrobbleBuffer(ctx)
for {
entry, err := buffer.Next(b.service, userId)
if err != nil {
log.Error(ctx, "Error reading from scrobble buffer", "scrobbler", b.service, err)
return false
}
if entry == nil {
return true
}
log.Debug(ctx, "Sending scrobble", "scrobbler", b.service, "track", entry.Title, "artist", entry.Artist)
err = b.wrapped.Scrobble(ctx, entry.UserID, Scrobble{
MediaFile: entry.MediaFile,
TimeStamp: entry.PlayTime,
})
if err != nil {
switch err {
case ErrRetryLater:
log.Warn(ctx, "Could not send scrobble. Will be retried", "userId", entry.UserID,
"track", entry.Title, "artist", entry.Artist, "scrobbler", b.service, err)
return false
default:
log.Error(ctx, "Error sending scrobble to service. Discarding", "scrobbler", b.service,
"userId", entry.UserID, "artist", entry.Artist, "track", entry.Title, err)
}
}
err = buffer.Dequeue(entry)
if err != nil {
log.Error(ctx, "Error removing entry from scrobble buffer", "userId", entry.UserID,
"track", entry.Title, "artist", entry.Artist, "scrobbler", b.service, err)
return false
}
}
}
var _ Scrobbler = (*bufferedScrobbler)(nil)

View File

@@ -0,0 +1,28 @@
package scrobbler
import (
"context"
"errors"
"time"
"github.com/navidrome/navidrome/model"
)
type Scrobble struct {
model.MediaFile
TimeStamp time.Time
}
var (
ErrNotAuthorized = errors.New("not authorized")
ErrRetryLater = errors.New("retry later")
ErrUnrecoverable = errors.New("unrecoverable")
)
type Scrobbler interface {
IsAuthorized(ctx context.Context, userId string) bool
NowPlaying(ctx context.Context, userId string, track *model.MediaFile) error
Scrobble(ctx context.Context, userId string, s Scrobble) error
}
type Constructor func(ds model.DataStore) Scrobbler

View File

@@ -0,0 +1,195 @@
package scrobbler
import (
"context"
"sort"
"time"
"github.com/navidrome/navidrome/conf"
"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/singleton"
)
const nowPlayingExpire = 60 * time.Minute
type NowPlayingInfo struct {
TrackID string
Start time.Time
Username string
PlayerId string
PlayerName string
}
type Submission struct {
TrackID string
Timestamp time.Time
}
type PlayTracker interface {
NowPlaying(ctx context.Context, playerId string, playerName string, trackId string) error
GetNowPlaying(ctx context.Context) ([]NowPlayingInfo, error)
Submit(ctx context.Context, submissions []Submission) error
}
type playTracker struct {
ds model.DataStore
broker events.Broker
playMap *ttlcache.Cache
scrobblers map[string]Scrobbler
}
func GetPlayTracker(ds model.DataStore, broker events.Broker) PlayTracker {
instance := singleton.Get(playTracker{}, func() interface{} {
m := ttlcache.NewCache()
m.SkipTTLExtensionOnHit(true)
_ = m.SetTTL(nowPlayingExpire)
p := &playTracker{ds: ds, playMap: m, broker: broker}
p.scrobblers = make(map[string]Scrobbler)
for name, constructor := range constructors {
s := constructor(ds)
if conf.Server.DevEnableBufferedScrobble {
s = newBufferedScrobbler(ds, s, name)
}
p.scrobblers[name] = s
}
return p
})
return instance.(*playTracker)
}
func (p *playTracker) NowPlaying(ctx context.Context, playerId string, playerName string, trackId string) error {
user, _ := request.UserFrom(ctx)
info := NowPlayingInfo{
TrackID: trackId,
Start: time.Now(),
Username: user.UserName,
PlayerId: playerId,
PlayerName: playerName,
}
_ = p.playMap.Set(playerId, info)
player, _ := request.PlayerFrom(ctx)
if player.ScrobbleEnabled {
p.dispatchNowPlaying(ctx, user.ID, trackId)
}
return nil
}
func (p *playTracker) dispatchNowPlaying(ctx context.Context, userId string, trackId string) {
t, err := p.ds.MediaFile(ctx).Get(trackId)
if err != nil {
log.Error(ctx, "Error retrieving mediaFile", "id", trackId, err)
return
}
// TODO Parallelize
for name, s := range p.scrobblers {
if !s.IsAuthorized(ctx, userId) {
continue
}
log.Debug(ctx, "Sending NowPlaying update", "scrobbler", name, "track", t.Title, "artist", t.Artist)
err := s.NowPlaying(ctx, userId, t)
if err != nil {
log.Error(ctx, "Error sending NowPlayingInfo", "scrobbler", name, "track", t.Title, "artist", t.Artist, err)
return
}
}
}
func (p *playTracker) GetNowPlaying(ctx 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)
}
sort.Slice(res, func(i, j int) bool {
return res[i].Start.After(res[j].Start)
})
return res, nil
}
func (p *playTracker) Submit(ctx context.Context, submissions []Submission) error {
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)
}
event := &events.RefreshResource{}
success := 0
for _, s := range submissions {
mf, err := p.ds.MediaFile(ctx).Get(s.TrackID)
if err != nil {
log.Error("Cannot find track for scrobbling", "id", s.TrackID, "user", username, err)
continue
}
err = p.incPlay(ctx, mf, s.Timestamp)
if err != nil {
log.Error("Error updating play counts", "id", mf.ID, "track", mf.Title, "user", username, err)
} else {
success++
event.With("song", mf.ID).With("album", mf.AlbumID).With("artist", mf.AlbumArtistID)
log.Info("Scrobbled", "title", mf.Title, "artist", mf.Artist, "user", username)
if player.ScrobbleEnabled {
_ = p.dispatchScrobble(ctx, mf, s.Timestamp)
}
}
}
if success > 0 {
p.broker.SendMessage(ctx, event)
}
return nil
}
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)
if err != nil {
return err
}
err = p.ds.Album(ctx).IncPlayCount(track.AlbumID, timestamp)
if err != nil {
return err
}
err = p.ds.Artist(ctx).IncPlayCount(track.ArtistID, timestamp)
return err
})
}
func (p *playTracker) dispatchScrobble(ctx context.Context, t *model.MediaFile, playTime time.Time) error {
u, _ := request.UserFrom(ctx)
scrobble := Scrobble{MediaFile: *t, TimeStamp: playTime}
for name, s := range p.scrobblers {
if !s.IsAuthorized(ctx, u.ID) {
continue
}
if conf.Server.DevEnableBufferedScrobble {
log.Debug(ctx, "Buffering scrobble", "scrobbler", name, "track", t.Title, "artist", t.Artist)
} else {
log.Debug(ctx, "Sending scrobble", "scrobbler", name, "track", t.Title, "artist", t.Artist)
}
err := s.Scrobble(ctx, u.ID, scrobble)
if err != nil {
log.Error(ctx, "Error sending Scrobble", "scrobbler", name, "track", t.Title, "artist", t.Artist, err)
return err
}
}
return nil
}
var constructors map[string]Constructor
func Register(name string, init Constructor) {
if constructors == nil {
constructors = make(map[string]Constructor)
}
constructors[name] = init
}

View File

@@ -0,0 +1,207 @@
package scrobbler
import (
"context"
"errors"
"time"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/server/events"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("PlayTracker", func() {
var ctx context.Context
var ds model.DataStore
var tracker PlayTracker
var track model.MediaFile
var album model.Album
var artist model.Artist
var fake fakeScrobbler
BeforeEach(func() {
// Remove buffering to simplify tests
conf.Server.DevEnableBufferedScrobble = false
ctx = context.Background()
ctx = request.WithUser(ctx, model.User{ID: "u-1"})
ctx = request.WithPlayer(ctx, model.Player{ScrobbleEnabled: true})
ds = &tests.MockDataStore{}
fake = fakeScrobbler{Authorized: true}
Register("fake", func(ds model.DataStore) Scrobbler {
return &fake
})
tracker = GetPlayTracker(ds, events.GetBroker())
track = model.MediaFile{
ID: "123",
Title: "Track Title",
Album: "Track Album",
AlbumID: "al-1",
Artist: "Track Artist",
ArtistID: "ar-1",
AlbumArtist: "Track AlbumArtist",
TrackNumber: 1,
Duration: 180,
MbzTrackID: "mbz-123",
}
_ = ds.MediaFile(ctx).Put(&track)
artist = model.Artist{ID: "ar-1"}
_ = ds.Artist(ctx).Put(&artist)
album = model.Album{ID: "al-1"}
_ = ds.Album(ctx).(*tests.MockAlbumRepo).Put(&album)
})
Describe("NowPlaying", func() {
It("sends track to agent", func() {
err := tracker.NowPlaying(ctx, "player-1", "player-one", "123")
Expect(err).ToNot(HaveOccurred())
Expect(fake.NowPlayingCalled).To(BeTrue())
Expect(fake.UserID).To(Equal("u-1"))
Expect(fake.Track.ID).To(Equal("123"))
})
It("does not send track to agent if user has not authorized", func() {
fake.Authorized = false
err := tracker.NowPlaying(ctx, "player-1", "player-one", "123")
Expect(err).ToNot(HaveOccurred())
Expect(fake.NowPlayingCalled).To(BeFalse())
})
It("does not send track to agent if player is not enabled to send scrobbles", func() {
ctx = request.WithPlayer(ctx, model.Player{ScrobbleEnabled: false})
err := tracker.NowPlaying(ctx, "player-1", "player-one", "123")
Expect(err).ToNot(HaveOccurred())
Expect(fake.NowPlayingCalled).To(BeFalse())
})
})
Describe("GetNowPlaying", func() {
BeforeEach(func() {
ctx = context.Background()
})
It("returns current playing music", func() {
track2 := track
track2.ID = "456"
_ = ds.MediaFile(ctx).Put(&track)
ctx = request.WithUser(ctx, model.User{UserName: "user-1"})
_ = tracker.NowPlaying(ctx, "player-1", "player-one", "123")
ctx = request.WithUser(ctx, model.User{UserName: "user-2"})
_ = tracker.NowPlaying(ctx, "player-2", "player-two", "456")
playing, err := tracker.GetNowPlaying(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(playing).To(HaveLen(2))
Expect(playing[0].PlayerId).To(Equal("player-2"))
Expect(playing[0].PlayerName).To(Equal("player-two"))
Expect(playing[0].Username).To(Equal("user-2"))
Expect(playing[0].TrackID).To(Equal("456"))
Expect(playing[1].PlayerId).To(Equal("player-1"))
Expect(playing[1].PlayerName).To(Equal("player-one"))
Expect(playing[1].Username).To(Equal("user-1"))
Expect(playing[1].TrackID).To(Equal("123"))
})
})
Describe("Submit", func() {
It("sends track to agent", func() {
ctx = request.WithUser(ctx, model.User{ID: "u-1", UserName: "user-1"})
ts := time.Now()
err := tracker.Submit(ctx, []Submission{{TrackID: "123", Timestamp: ts}})
Expect(err).ToNot(HaveOccurred())
Expect(fake.ScrobbleCalled).To(BeTrue())
Expect(fake.UserID).To(Equal("u-1"))
Expect(fake.LastScrobble.ID).To(Equal("123"))
})
It("increments play counts in the DB", func() {
ctx = request.WithUser(ctx, model.User{ID: "u-1", UserName: "user-1"})
ts := time.Now()
err := tracker.Submit(ctx, []Submission{{TrackID: "123", Timestamp: ts}})
Expect(err).ToNot(HaveOccurred())
Expect(track.PlayCount).To(Equal(int64(1)))
Expect(album.PlayCount).To(Equal(int64(1)))
Expect(artist.PlayCount).To(Equal(int64(1)))
})
It("does not send track to agent if user has not authorized", func() {
fake.Authorized = false
err := tracker.Submit(ctx, []Submission{{TrackID: "123", Timestamp: time.Now()}})
Expect(err).ToNot(HaveOccurred())
Expect(fake.ScrobbleCalled).To(BeFalse())
})
It("does not send track to agent player is not enabled to send scrobbles", func() {
ctx = request.WithPlayer(ctx, model.Player{ScrobbleEnabled: false})
err := tracker.Submit(ctx, []Submission{{TrackID: "123", Timestamp: time.Now()}})
Expect(err).ToNot(HaveOccurred())
Expect(fake.ScrobbleCalled).To(BeFalse())
})
It("increments play counts even if it cannot scrobble", func() {
fake.Error = errors.New("error")
err := tracker.Submit(ctx, []Submission{{TrackID: "123", Timestamp: time.Now()}})
Expect(err).ToNot(HaveOccurred())
Expect(fake.ScrobbleCalled).To(BeFalse())
Expect(track.PlayCount).To(Equal(int64(1)))
Expect(album.PlayCount).To(Equal(int64(1)))
Expect(artist.PlayCount).To(Equal(int64(1)))
})
})
})
type fakeScrobbler struct {
Authorized bool
NowPlayingCalled bool
ScrobbleCalled bool
UserID string
Track *model.MediaFile
LastScrobble Scrobble
Error error
}
func (f *fakeScrobbler) IsAuthorized(ctx context.Context, userId string) bool {
return f.Error == nil && f.Authorized
}
func (f *fakeScrobbler) NowPlaying(ctx context.Context, userId string, track *model.MediaFile) error {
f.NowPlayingCalled = true
if f.Error != nil {
return f.Error
}
f.UserID = userId
f.Track = track
return nil
}
func (f *fakeScrobbler) Scrobble(ctx context.Context, userId string, s Scrobble) error {
f.ScrobbleCalled = true
if f.Error != nil {
return f.Error
}
f.UserID = userId
f.LastScrobble = s
return nil
}

View File

@@ -0,0 +1,17 @@
package scrobbler
import (
"testing"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestAgents(t *testing.T) {
tests.Init(t, false)
log.SetLevel(log.LevelCritical)
RegisterFailHandler(Fail)
RunSpecs(t, "Scrobbler Test Suite")
}

54
core/share.go Normal file
View File

@@ -0,0 +1,54 @@
package core
import (
"context"
"github.com/deluan/rest"
gonanoid "github.com/matoous/go-nanoid"
"github.com/navidrome/navidrome/model"
)
type Share interface {
NewRepository(ctx context.Context) rest.Repository
}
func NewShare(ds model.DataStore) Share {
return &shareService{
ds: ds,
}
}
type shareService struct {
ds model.DataStore
}
func (s *shareService) NewRepository(ctx context.Context) rest.Repository {
repo := s.ds.Share(ctx)
wrapper := &shareRepositoryWrapper{
ShareRepository: repo,
Repository: repo.(rest.Repository),
Persistable: repo.(rest.Persistable),
}
return wrapper
}
type shareRepositoryWrapper struct {
model.ShareRepository
rest.Repository
rest.Persistable
}
func (r *shareRepositoryWrapper) Save(entity interface{}) (string, error) {
s := entity.(*model.Share)
id, err := gonanoid.Generate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 9)
if err != nil {
return "", err
}
s.Name = id
id, err = r.Persistable.Save(s)
return id, err
}
func (r *shareRepositoryWrapper) Update(entity interface{}, _ ...string) error {
return r.Persistable.Update(entity, "description")
}

51
core/share_test.go Normal file
View File

@@ -0,0 +1,51 @@
package core
import (
"context"
"github.com/deluan/rest"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Share", func() {
var ds model.DataStore
var share Share
var mockedRepo rest.Persistable
BeforeEach(func() {
ds = &tests.MockDataStore{}
mockedRepo = ds.Share(context.Background()).(rest.Persistable)
share = NewShare(ds)
})
Describe("NewRepository", func() {
var repo rest.Persistable
BeforeEach(func() {
repo = share.NewRepository(context.Background()).(rest.Persistable)
})
Describe("Save", func() {
It("it adds a random name", func() {
entity := &model.Share{Description: "test"}
id, err := repo.Save(entity)
Expect(err).ToNot(HaveOccurred())
Expect(id).ToNot(BeEmpty())
Expect(entity.Name).ToNot(BeEmpty())
})
})
Describe("Update", func() {
It("filters out read-only fields", func() {
entity := "entity"
err := repo.Update(entity)
Expect(err).ToNot(HaveOccurred())
Expect(mockedRepo.(*tests.MockShareRepo).Entity).To(Equal("entity"))
Expect(mockedRepo.(*tests.MockShareRepo).Cols).To(ConsistOf("description"))
})
})
})
})

View File

@@ -8,7 +8,7 @@ import (
"strconv"
"strings"
"github.com/deluan/navidrome/log"
"github.com/navidrome/navidrome/log"
)
type Transcoder interface {
@@ -16,20 +16,15 @@ type Transcoder interface {
}
func New() Transcoder {
path, err := exec.LookPath("ffmpeg")
if err != nil {
log.Error("Unable to find ffmpeg", err)
}
log.Debug("Found ffmpeg", "path", path)
return &ffmpeg{}
return &externalTranscoder{}
}
type ffmpeg struct{}
type externalTranscoder struct{}
func (ff *ffmpeg) Start(ctx context.Context, command, path string, maxBitRate int) (f io.ReadCloser, err error) {
func (e *externalTranscoder) Start(ctx context.Context, command, path string, maxBitRate int) (f io.ReadCloser, err error) {
args := createTranscodeCommand(command, path, maxBitRate)
log.Trace(ctx, "Executing ffmpeg command", "cmd", args)
log.Trace(ctx, "Executing transcoding command", "cmd", args)
cmd := exec.Command(args[0], args[1:]...) // #nosec
cmd.Stderr = os.Stderr
if f, err = cmd.StdoutPipe(); err != nil {
@@ -48,8 +43,8 @@ func (ff *ffmpeg) Start(ctx context.Context, command, path string, maxBitRate in
func createTranscodeCommand(cmd, path string, maxBitRate int) []string {
split := strings.Split(cmd, " ")
for i, s := range split {
s = strings.Replace(s, "%s", path, -1)
s = strings.Replace(s, "%b", strconv.Itoa(maxBitRate), -1)
s = strings.ReplaceAll(s, "%s", path)
s = strings.ReplaceAll(s, "%b", strconv.Itoa(maxBitRate))
split[i] = s
}

View File

@@ -3,8 +3,8 @@ package transcoder
import (
"testing"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/tests"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

View File

@@ -1,14 +1,23 @@
package core
import (
"github.com/deluan/navidrome/core/transcoder"
"github.com/google/wire"
"github.com/navidrome/navidrome/core/agents"
"github.com/navidrome/navidrome/core/scrobbler"
"github.com/navidrome/navidrome/core/transcoder"
)
var Set = wire.NewSet(
NewCover,
NewArtwork,
NewMediaStreamer,
NewTranscodingCache,
NewImageCache,
GetTranscodingCache,
GetImageCache,
NewArchiver,
NewExternalMetadata,
NewCacheWarmer,
NewPlayers,
agents.New,
transcoder.New,
scrobbler.GetPlayTracker,
NewShare,
)

View File

@@ -2,13 +2,14 @@ package db
import (
"database/sql"
"fmt"
"os"
"sync"
"github.com/deluan/navidrome/conf"
_ "github.com/deluan/navidrome/db/migration"
"github.com/deluan/navidrome/log"
_ "github.com/mattn/go-sqlite3"
"github.com/navidrome/navidrome/conf"
_ "github.com/navidrome/navidrome/db/migration"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/utils/singleton"
"github.com/pressly/goose"
)
@@ -17,32 +18,42 @@ var (
Path string
)
var (
once sync.Once
db *sql.DB
)
func Db() *sql.DB {
once.Do(func() {
var err error
instance := singleton.Get(&sql.DB{}, func() interface{} {
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)
db, err = sql.Open(Driver, Path)
instance, err := sql.Open(Driver, Path)
if err != nil {
panic(err)
}
return instance
})
return db
return instance.(*sql.DB)
}
func EnsureLatestVersion() {
db := Db()
err := goose.SetDialect(Driver)
// Disable foreign_keys to allow re-creating tables in migrations
_, err := db.Exec("PRAGMA foreign_keys=off")
defer func() {
_, err := db.Exec("PRAGMA foreign_keys=on")
if err != nil {
log.Error("Error re-enabling foreign_keys", err)
}
}()
if err != nil {
log.Error("Error disabling foreign_keys", err)
}
gooseLogger := &logAdapter{silent: isSchemaEmpty(db)}
goose.SetLogger(gooseLogger)
err = goose.SetDialect(Driver)
if err != nil {
log.Error("Invalid DB driver", "driver", Driver, err)
os.Exit(1)
@@ -53,3 +64,45 @@ func EnsureLatestVersion() {
os.Exit(1)
}
}
func isSchemaEmpty(db *sql.DB) bool { // nolint:interfacer
rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table' AND name='goose_db_version';") // nolint:rowserrcheck
if err != nil {
log.Error("Database could not be opened!", err)
os.Exit(1)
}
defer rows.Close()
return !rows.Next()
}
type logAdapter struct {
silent bool
}
func (l *logAdapter) Fatal(v ...interface{}) {
log.Error(fmt.Sprint(v...))
os.Exit(-1)
}
func (l *logAdapter) Fatalf(format string, v ...interface{}) {
log.Error(fmt.Sprintf(format, v...))
os.Exit(-1)
}
func (l *logAdapter) Print(v ...interface{}) {
if !l.silent {
log.Info(fmt.Sprint(v...))
}
}
func (l *logAdapter) Println(v ...interface{}) {
if !l.silent {
log.Info(fmt.Sprintln(v...))
}
}
func (l *logAdapter) Printf(format string, v ...interface{}) {
if !l.silent {
log.Info(fmt.Sprintf(format, v...))
}
}

36
db/db_test.go Normal file
View File

@@ -0,0 +1,36 @@
package db
import (
"database/sql"
"testing"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestDB(t *testing.T) {
tests.Init(t, false)
log.SetLevel(log.LevelCritical)
RegisterFailHandler(Fail)
RunSpecs(t, "DB Suite")
}
var _ = Describe("isSchemaEmpty", func() {
var db *sql.DB
BeforeEach(func() {
path := "file::memory:"
db, _ = sql.Open(Driver, path)
})
It("returns false if the goose metadata table is found", func() {
_, err := db.Exec("create table goose_db_version (id primary key);")
Expect(err).ToNot(HaveOccurred())
Expect(isSchemaEmpty(db)).To(BeFalse())
})
It("returns true if the schema is brand new", func() {
Expect(isSchemaEmpty(db)).To(BeTrue())
})
})

View File

@@ -1,9 +1,9 @@
package migration
package migrations
import (
"database/sql"
"github.com/deluan/navidrome/log"
"github.com/navidrome/navidrome/log"
"github.com/pressly/goose"
)

View File

@@ -1,4 +1,4 @@
package migration
package migrations
import (
"database/sql"

View File

@@ -1,4 +1,4 @@
package migration
package migrations
import (
"database/sql"

View File

@@ -1,4 +1,4 @@
package migration
package migrations
import (
"database/sql"

View File

@@ -1,4 +1,4 @@
package migration
package migrations
import (
"database/sql"

View File

@@ -1,4 +1,4 @@
package migration
package migrations
import (
"database/sql"

View File

@@ -1,4 +1,4 @@
package migration
package migrations
import (
"database/sql"

View File

@@ -1,4 +1,4 @@
package migration
package migrations
import (
"database/sql"

View File

@@ -1,4 +1,4 @@
package migration
package migrations
import (
"database/sql"

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