Compare commits

...

1242 Commits

Author SHA1 Message Date
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
Deluan
d268075046 Change the default scanner to use new implementation 2020-07-19 21:39:06 -04:00
Deluan
482f46f3fd Remove unneeded context in log calls 2020-07-19 15:28:50 -04:00
Deluan
f0160f5d2a Rate limit login attempts using a Sliding Window counter rate-limiter 2020-07-19 14:45:05 -04:00
Deluan
feca030c6d Give warning when playlists are not imported due to not having an admin user 2020-07-19 13:58:46 -04:00
Deluan
41138bd665 Only show auto-import info for auto-imported playlists 2020-07-18 01:03:44 -04:00
Deluan
178e42487b Remove invalid config options 2020-07-17 23:16:04 -04:00
dependabot[bot]
ae04919585 Bump lodash from 4.17.15 to 4.17.19 in /ui
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-17 22:58:51 -04:00
Deluan
6adba03868 Renamed misleading function name 2020-07-17 22:55:51 -04:00
Deluan
609d172259 Use first admin user for all scan operations 2020-07-17 22:55:51 -04:00
Deluan
9cf8c92cae Break up processChangedDir into smaller functions 2020-07-17 22:55:51 -04:00
Deluan
38c3013ddf Add auto-import fields to the UI 2020-07-17 22:55:51 -04:00
Deluan
8f512a40f7 Refactored playlist auto-import support 2020-07-17 22:55:51 -04:00
Deluan
b9b6ce066b Auto-Import playlists found in the Music Folder 2020-07-17 22:55:51 -04:00
Deluan
35114be5f7 Add path to playlist 2020-07-17 22:55:51 -04:00
Deluan
3239be4a4d Change log level of some scanner operations 2020-07-17 12:49:37 -04:00
Deluan
a706cb46fa Fix pre-push hook 2020-07-17 12:16:23 -04:00
Deluan
3095bee5d9 Fix lint error 2020-07-17 12:16:16 -04:00
Deluan
51c295d1de Add new scanner algorithm, can be enabled with DevNewScanner config option 2020-07-17 12:06:49 -04:00
Deluan
de0cc1f268 Move LoadAllAudioFiles tests to the proper test file 2020-07-16 18:18:48 -04:00
Deluan
037f6b606e Replace lefthook with shell script 2020-07-16 18:14:02 -04:00
Deluan
e7f6ba8f35 Move LoadAllAudioFiles function to the right file 2020-07-16 17:42:26 -04:00
Deluan
25f68b6c89 If mediafile does not have an embedded coverart, use album's 2020-07-16 17:08:52 -04:00
Deluan
dc50f672b8 Fix Makefile target name 2020-07-16 16:57:19 -04:00
Deluan
d14a6031f0 Add test for case-sensitive DeleteByPath 2020-07-14 15:35:42 -04:00
Deluan
8b20c26e04 Make "ByPath" queries case-sensitive 2020-07-14 15:27:27 -04:00
Deluan
1ef0869a54 Strip debugging info from binaries. Closes #405 2020-07-14 13:58:39 -04:00
Deluan Quintão
ca10e800a9 Add demo site to README.md 2020-07-14 07:59:14 -04:00
Deluan
33d5459c20 Escape paths in "ByPath" queries 2020-07-14 07:20:27 -04:00
Deluan
aae43f4452 Remove unneeded \n 2020-07-13 11:49:06 -04:00
Deluan
0bd842869b go mod tidy 2020-07-13 09:35:39 -04:00
Deluan
394d3b0e67 Turn off Go 1.14 async preemption as it causes issues with CIFS/SMB access. See #393 2020-07-12 22:09:51 -04:00
Deluan
1ef17e2986 Remove version command 2020-07-12 20:44:19 -04:00
Deluan
d4347f20ae Remove redundant log message 2020-07-12 20:42:38 -04:00
Deluan
3319f78de0 Remove unnecessary config from docker images 2020-07-12 14:09:32 -04:00
Deluan
ee0ae0a06c Fix lint errors 2020-07-12 13:36:22 -04:00
Deluan
064da8e034 Add more trace logging to scanner 2020-07-12 13:30:03 -04:00
Deluan
74cf0ee1c1 Create Data Folder if it does not exist 2020-07-12 12:36:08 -04:00
Deluan
c2f40ea8a3 Show totals at the end of scan 2020-07-12 12:35:23 -04:00
Deluan
f694e471fb Make private types unexported 2020-07-12 11:55:19 -04:00
Deluan
dc8368c89c Return counter from DeleteByPath 2020-07-12 11:53:07 -04:00
Deluan
e55397fcdc Bump github.com/onsi/ginkgo from 1.13.0 to 1.14.0 2020-07-12 11:24:55 -04:00
Deluan
8260b46e8f Fix migration 2020-07-12 11:22:24 -04:00
Deluan
b59c6c85e0 Add support for armv5. Closes #395 2020-07-12 09:43:18 -04:00
Deluan
b96ff9c210 Use ci-goreleaser 1.14.4-2
This should generate binaries compatible with OpenVZ (kernel 2.6.32)
2020-07-12 09:43:18 -04:00
Deluan
c758780e38 Remove MUSL build 2020-07-11 14:33:23 -04:00
Deluan
9e35534dad Fix lint errors
New environment, forgot to setup it properly...
2020-07-10 13:11:02 -04:00
Deluan
5620c58a30 Started the big refactor to extract common logic from engine package (Subsonic only) to core package (more generic) 2020-07-10 12:53:11 -04:00
Deluan
5418a6b6b1 Remove unused docker files 2020-07-09 00:45:04 -04:00
Deluan
865bad1550 Send play song event to GA 2020-07-08 21:23:51 -04:00
Deluan
7c3fd38559 Add option to change IP address to bind 2020-07-08 20:54:56 -04:00
Deluan Quintão
933052583a Update FUNDING.yml 2020-07-08 13:07:08 -04:00
Deluan Quintão
941e252d44 Update FUNDING.yml 2020-07-08 12:57:46 -04:00
Deluan
f0a5df7cd7 Move transcodings initialization to a migration
This will make it run only once, not every
time the transcoding table is empty
2020-07-06 23:48:43 -04:00
Deluan
fdc38b5ca5 Enable DSD (.dsf) support 2020-07-06 12:25:55 -04:00
Deluan
2f8b01015d Change log level for "path unavailable" 2020-07-04 11:36:57 -04:00
Deluan
2a302de42f Set default session timeout to 24h (agan) 2020-07-03 22:08:32 -04:00
Deluan
681849d174 Fix pls ignoring 2020-07-03 21:15:01 -04:00
Deluan
17830d63b4 Ignore m3u files when scanning 2020-07-03 21:06:33 -04:00
Deluan
1cc03fdd8c Add initial support for Google Analytics 2020-07-03 13:51:31 -04:00
Deluan
dd91f983b5 Add new config option to show a custom welcome message in the login screen 2020-07-03 11:51:15 -04:00
Deluan
3a7d70c908 Add scan command 2020-07-03 10:49:11 -04:00
Deluan
8181aba61f Clean up a bit 2020-07-03 10:19:44 -04:00
Deluan
2d0539300d Exit if specified config file is not present 2020-07-03 10:10:49 -04:00
Deluan
f45045d1c0 Bump viper version to 1.7.0 2020-07-03 09:49:52 -04:00
Deluan
6954e1b4eb Fix linting error 2020-07-03 09:46:58 -04:00
Deluan
ef9af6ed1a Don't fail if config file isnot found 2020-07-03 09:39:28 -04:00
Deluan
99e269208e Fix lint errors 2020-07-02 18:17:31 -04:00
Deluan
f980e24868 Add missing wire file 2020-07-02 18:17:26 -04:00
Deluan
a65c9bbb16 Refactor and clean up 2020-07-02 17:53:51 -04:00
Deluan
d2e4cade62 Change duration config types 2020-07-02 17:53:51 -04:00
Deluan
5021c0fd0c Replace multiconfig with cobra+viper 2020-07-02 17:53:51 -04:00
Deluan Quintão
fea060e4f2 Update FUNDING.yml 2020-07-02 11:57:52 -04:00
Deluan
7a9b848f38 Add quality to image cache key 2020-07-01 17:02:27 -04:00
Deluan Quintão
2d8f0a740e Add FUNDING.yml 2020-07-01 12:31:04 -04:00
Deluan
fa107a6b65 Bump Beego version to v1.12.2 2020-07-01 10:00:19 -04:00
Deluan
2371e9b943 Add option to set jpeg quality level. Closes #371 2020-06-29 17:20:38 -04:00
Deluan
f0ee52a98e Fix album refresh query. Fixes #373 2020-06-29 14:17:28 -04:00
Deluan
c01d81802d Fix album's songCount. Fixes #373 2020-06-29 11:35:51 -04:00
Deluan
890ca64f51 Fix cover.jpg discovery 2020-06-29 10:50:38 -04:00
Deluan
bcaf330233 Make sure to select cover art from media_file that has it. Fix #360 2020-06-27 22:16:07 -04:00
Deluan
ab1c943d1f Force album/artist refresh when folder changes, to cater for cover art files 2020-06-27 18:41:55 -04:00
Deluan
703875b895 Fallback to album art if mediaFile does not have cover art 2020-06-27 13:11:51 -04:00
Deluan
5f40801a78 Add more logs to GC call 2020-06-26 10:23:05 -04:00
Deluan
eb109ebeb4 Remove duplicated helper functions, move them to utils package 2020-06-24 20:48:42 -04:00
Alex Palaistras
bb9a7fadc0 Add tests for external album cover processing
This implements basic tests for functionality related to loading and
processing external album covers, both on the scanning size, and on the
display side.
2020-06-24 20:48:42 -04:00
Alex Palaistras
ac5d99c079 Check MIME type for cover on refresh, display
Files that match the `CoverArtPriority` setting will now be considered
eligible only if their extensions are of an 'image/*' MIME type (e.g.
'.png' for 'image/png', '.jpg' for 'image/jpeg'). This prevents matching
files that will likely not be valid during display.

In addition to the above, code for returning the cover image file from
scanned data will also check against the MIME type for the path stored,
instead of attempting to re-trace `CoverArtPriority` matches. This
simplifies the code and bypasses a number of edge-cases related to
inconsistent matching.
2020-06-24 20:48:42 -04:00
Alex Palaistras
d9c991e325 Return error when no matching cover is found
When checking stored references to cover images (whether embedded or
external), it's possible that configured patterns do no match, and a
valid error should be returned in those cases.
2020-06-24 20:48:42 -04:00
Alex Palaistras
08cd28af2d Load cover art from file directory
This commit adds support for loading cover art from media file
directories, according to configured filename priorities (of which an
additional, special choice of `embedded` is given).

Cover art paths are resolved during scanning and stored in the database
as part of the `album.cover_art_path` column; if embedded cover art is
matched, this will default to the path of the media file itself, and if
no cover art is matched at all.

Similarly, the `album.cover_art_id` column will default to a reference
to `media_file.id` if embedded cover art is wanted, but if an external
cover art file is matched, this will instead be set to a reference to
the `album.id` value itself, prefixed with the `al-` constant.

Stored cover art paths are once again resolved and matched against
configuration when covers are requested; that is, any change in
configuration between scanning and requesting cover art may not return
correct data until a re-scan is complete.

Tests will be added in future commits.
2020-06-24 20:48:42 -04:00
Deluan
6563897692 Restore volume level after a refresh 2020-06-24 15:33:59 -04:00
Deluan Quintão
04d598819d Update tr.json (POEditor.com) 2020-06-23 16:53:55 -04:00
Deluan Quintão
965c04469e Update it.json (POEditor.com) 2020-06-23 16:53:55 -04:00
Deluan Quintão
416ca2c063 Update de.json (POEditor.com) 2020-06-23 16:53:55 -04:00
Deluan Quintão
ab35586b0c Update fr.json (POEditor.com) 2020-06-23 16:53:55 -04:00
Deluan Quintão
acb5985127 Update cs.json (POEditor.com) 2020-06-23 16:53:55 -04:00
Deluan
9b75b729ba Convert function to arrow-function 2020-06-22 19:55:04 -04:00
dependabot-preview[bot]
e1968b0953 Bump @testing-library/user-event from 11.2.0 to 12.0.6 in /ui
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 11.2.0 to 12.0.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/v11.2.0...v12.0.6)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-22 10:58:37 -04:00
Deluan
f36e15cfeb Upgrade dependencies 2020-06-22 10:23:26 -04:00
Deluan
7547c775fa Bump React-Admin version to 3.6.1 2020-06-22 10:17:55 -04:00
dependabot-preview[bot]
ad21b5f0d0 Bump react-drag-listview from 0.1.6 to 0.1.7 in /ui
Bumps [react-drag-listview](https://github.com/raisezhang/react-drag-listview) from 0.1.6 to 0.1.7.
- [Release notes](https://github.com/raisezhang/react-drag-listview/releases)
- [Commits](https://github.com/raisezhang/react-drag-listview/compare/0.1.6...0.1.7)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-22 09:31:30 -04:00
Deluan
4427900d84 Fix formatting 2020-06-22 08:56:55 -04:00
Deluan
0ca70b1e4d Add back Artist column header to Album List View. Fixes #363 2020-06-22 08:50:41 -04:00
Deluan
0292a334fe Fix mistranslation 2020-06-22 00:11:19 -04:00
Deluan
f93e2d0c04 Use memoization to avoid re-renders 2020-06-19 19:43:33 -04:00
Deluan
3a9324c6ef Enable autoPlay in React Player 2020-06-19 16:32:54 -04:00
Deluan
cf692140a9 Revert "Upgrade to React Player 4.15.1"
This reverts commit de693b8206. (+1 squashed commit)
Squashed commits:
[cc80cb8] Revert "Simplify handle"

This reverts commit 83b8fa14c6.
2020-06-19 16:31:38 -04:00
Deluan
83b8fa14c6 Simplify handle 2020-06-19 12:37:26 -04:00
Deluan
de693b8206 Upgrade to React Player 4.15.1 2020-06-19 12:24:11 -04:00
Deluan
1686e358fe Simplify PlayQueue store 2020-06-19 11:32:24 -04:00
Deluan
804d969427 Clear play queue on login and logout 2020-06-19 11:32:23 -04:00
Deluan
9d23b191b5 Show indicator on current playing song. Fixes #128 2020-06-19 11:32:23 -04:00
Deluan
eb4c0f0b84 go mod tidy 2020-06-18 12:51:15 -04:00
dependabot-preview[bot]
c507e344ff Bump github.com/onsi/ginkgo from 1.12.3 to 1.13.0
Bumps [github.com/onsi/ginkgo](https://github.com/onsi/ginkgo) from 1.12.3 to 1.13.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.12.3...v1.13.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-15 09:16:51 -04:00
Deluan
a6af46dbad Always use lowercase username, as it is used for referential integrity. Fixes #352 2020-06-14 20:20:10 -04:00
Deluan
2d1d992e17 Support Windows paths 2020-06-14 03:11:16 -04:00
Deluan
653b5ea9d3 Replace map[string]bool with map[string]struct{} 2020-06-14 03:11:16 -04:00
Deluan
e73b71aaf7 Remove tracks from DB that were deleted while Navidrome was not running. Fixes #151 2020-06-14 03:11:16 -04:00
Deluan
01919661e9 Skip unreadable directories. Fixes #328 2020-06-14 03:11:16 -04:00
Deluan
3190611ec8 Call ffmpeg in batches 2020-06-14 03:11:16 -04:00
Deluan
6a3dabbb06 Optimize queries by path 2020-06-14 03:11:16 -04:00
Deluan
238020c839 Handle folders with lots of albums and/or artists 2020-06-14 03:11:16 -04:00
Deluan
72b2e756f7 Revert "Show indicator on current playing song. Fixes #128"
This implementation causes performance issues
2020-06-13 16:41:11 -04:00
Deluan
86bc8d97a0 Support dark themes in "Playing" indicator 2020-06-13 14:38:25 -04:00
Deluan
003b73fe1a Remove invalid propType 2020-06-13 14:04:45 -04:00
Deluan
be2afb94ae Show indicator on current playing song. Fixes #128 2020-06-13 14:04:45 -04:00
Deluan
f8a18b59b0 Add link to album from player's song title. Fixes #324 2020-06-12 17:02:13 -04:00
Deluan Quintão
c216b14655 Add total downloads badge 2020-06-12 14:21:02 -04:00
Deluan
4702c5abbd Add track/artist being played to the page title. Closes #317 2020-06-11 22:40:35 -04:00
Deluan
c742ae0843 Remove unused feature toggles 2020-06-11 22:11:59 -04:00
Deluan
0033966c25 No need to delete the playlist tracks explicitly 2020-06-10 18:07:10 -04:00
Deluan
f072ffd377 Add confirmation when deleting user 2020-06-10 18:07:10 -04:00
Deluan
94d88395e7 Add referential integrity to player and playlist tables 2020-06-10 18:07:10 -04:00
Deluan
c9bcb333ae Add more options to Players list 2020-06-10 18:07:10 -04:00
Deluan Quintão
84ed3eb427 Update README.md 2020-06-10 11:34:51 -04:00
Deluan
8bd9787c51 Fix function naming 2020-06-09 20:45:53 -04:00
Deluan
1c466d6083 Fix formatting 2020-06-09 20:34:36 -04:00
Deluan
a64b15c174 Fix navigation issues caused by the use of useListParams 2020-06-09 20:29:12 -04:00
Deluan
7148741a4f Revert "Keep image aspect ratio when resizing"
This reverts commit 50f4bd86
2020-06-09 19:36:19 -04:00
Deluan
630c71119a Use fix for Opus cover art from https://github.com/dhowden/tag/pull/69 2020-06-09 17:08:05 -04:00
Deluan
50f4bd86a3 Keep image aspect ratio when resizing 2020-06-09 10:34:47 -04:00
Deluan
44c74f42e1 Add clickToPlay functionality to playlists 2020-06-09 08:54:11 -04:00
Deluan
29c7513879 Update updated_at field when modifying the playlist 2020-06-09 07:55:35 -04:00
Deluan
82d437f004 Better defaults to sort orders in List views 2020-06-09 07:46:28 -04:00
Deluan
b54d4c75ae Show year in album tile if album grid is filtered bu artist 2020-06-08 20:37:18 -04:00
Deluan
b636565c62 Disable public toggle if user is not the playlist's owner 2020-06-08 19:19:38 -04:00
Deluan
b4e06c416d Allow toggling a playlist public from the Playlist list view. Closes #344 2020-06-08 18:39:31 -04:00
dependabot-preview[bot]
5e2d463129 Bump @testing-library/user-event from 10.4.1 to 11.2.0 in /ui
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 10.4.1 to 11.2.0.
- [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/v10.4.1...v11.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-05 13:42:40 -04:00
Deluan
12d5d9573e Bum @testing-library dependencies 2020-06-05 13:34:46 -04:00
dependabot-preview[bot]
42ee8b64cb [Security] Bump websocket-extensions from 0.1.3 to 0.1.4 in /ui
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4. **This update includes a security fix.**
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-05 13:31:47 -04:00
Deluan
3908ad2681 Upgrade ReactAdmin to 3.6.0 2020-06-05 12:13:50 -04:00
Deluan
e9115dab4c Allow Writable to have multiple children 2020-06-05 11:55:30 -04:00
Deluan
79cf33281c Redirect to Playlists list after creating or editing 2020-06-05 11:55:30 -04:00
Deluan
2adb290c34 Do not show a "loading" datagrid for an empty playlist 2020-06-05 11:55:29 -04:00
Deluan
c6f23139bc Handle playlist's permissions on server 2020-06-05 11:55:29 -04:00
Deluan
4906b816af Only allows adding to a writable playlist 2020-06-05 10:26:53 -04:00
Deluan
39afe0c669 Check permissions for playlists 2020-06-05 10:22:31 -04:00
Deluan
f8a7ef1e19 Fix typo 2020-06-04 20:13:25 -04:00
Deluan
4776dba003 Make cursor=move for the whole playlist item row 2020-06-04 19:44:26 -04:00
Deluan
331fa1d952 Add ability to reorder playlist items 2020-06-04 19:05:41 -04:00
Deluan
b597a34cb4 Remove flickering when loading/refreshing Playlist show view 2020-06-04 16:54:30 -04:00
Deluan
51fb1d1349 Increase cover art max-age to maximum 2020-06-04 14:45:00 -04:00
Deluan
8fd86def18 Bump ginkgo version to 1.12.3 2020-06-03 09:43:34 -04:00
Deluan
5d285f92f5 Bump chi version to 4.1.2 2020-06-03 09:42:16 -04:00
Deluan
888151728f Increase album art placeholder's resolution 2020-06-03 09:40:37 -04:00
Deluan
b836dfe7f4 Do not reset the SongList query params 2020-05-31 14:27:02 -04:00
Deluan
ddcfc546fb Link is not on the album cover, leaving a gap between albums.
Other small improvements
2020-05-31 13:57:17 -04:00
Deluan
86a9f9e410 Show album info on hover 2020-05-30 19:42:08 -04:00
Deluan
14d7a69088 Fix context menu "display on hover" in playlists 2020-05-30 11:18:01 -04:00
Deluan
35e4eec293 Add album to playlist 2020-05-30 11:17:33 -04:00
Deluan
7547888f10 Change default session timeout to 24h 2020-05-30 10:34:16 -04:00
Deluan
fbedbb7893 Fix context menu on mobile, removed console warnings 2020-05-29 22:50:33 -04:00
Deluan
a7640c9df4 Optimized call to retrieve album songs 2020-05-29 17:34:54 -04:00
Deluan
8f8d992da4 Only add to playlist songs from selected discNumber (if present) 2020-05-29 16:42:13 -04:00
Deluan
3fe8b02cbd Make album context menu only visible on hover 2020-05-29 12:33:50 -04:00
Deluan
ba8c8725dd Refactor: move multiDisc detection logic to SongDatagrid 2020-05-29 12:20:17 -04:00
Deluan
915b701e44 Add context menu to individual discs in a set 2020-05-29 12:08:07 -04:00
Deluan
596100b58d Refactor: improve readability 2020-05-29 11:21:53 -04:00
Deluan
d8699b03bd Fix album sort fields 2020-05-28 20:48:58 -04:00
Deluan
7b36096153 Fix class of disc subtitle row 2020-05-28 09:25:53 -04:00
Deluan
62290bca77 Remove extra , 2020-05-28 08:16:31 -04:00
Deluan
498e196d48 Allow playing one disc of a set, by clicking on its number/name 2020-05-27 21:07:51 -04:00
Deluan Quintão
432fe10a5e Update tr.json (POEditor.com) 2020-05-27 09:48:04 -04:00
Deluan Quintão
7e625d68b5 Update de.json (POEditor.com) 2020-05-27 09:48:04 -04:00
Deluan
50f3a2c11d Upgrade Node to v14 2020-05-27 05:35:25 -04:00
Deluan
9028d301f0 Change log level for playlist log messages 2020-05-26 22:03:25 -04:00
Deluan
26dba27778 Always show song context menu on tablets 2020-05-26 22:02:15 -04:00
Deluan
7170485d08 Rename property 2020-05-26 17:59:04 -04:00
Deluan
2c68ba3934 only show playlist tracks' context menu on hover 2020-05-26 16:18:28 -04:00
Deluan
201a22e613 Change index in playlist to start from 1 2020-05-26 13:50:15 -04:00
Deluan Quintão
3ca295c863 Update it.json (POEditor.com) 2020-05-25 23:23:36 -04:00
Deluan Quintão
be85fe3773 Update de.json (POEditor.com) 2020-05-25 23:23:36 -04:00
Deluan Quintão
7c3d96cf6c Update fr.json (POEditor.com) 2020-05-25 23:23:36 -04:00
Deluan Quintão
50b44c1991 Update cs.json (POEditor.com) 2020-05-25 23:23:36 -04:00
Deluan
f9dae2dd2a Added individual AddToPlaylistDialogs to each list view 2020-05-25 22:51:31 -04:00
Deluan
00811f8000 Cancel the dialog when clicking the backdrop 2020-05-25 22:51:31 -04:00
Deluan
9c940cd44f Show AutomcompleteInput even if the list of playlists is not loaded yet 2020-05-25 22:51:31 -04:00
Deluan
1607dc8b88 Remove unused dependency 2020-05-25 22:51:31 -04:00
Deluan
a42a16696e Translate messages 2020-05-25 22:51:31 -04:00
Deluan
6db63e4dfc Use creatable autocomplete, to select or create a new playlist 2020-05-25 22:51:31 -04:00
Deluan
23bd5e1131 First version of dialog 2020-05-25 22:51:31 -04:00
Deluan
8973477fe5 npm audit fix 2020-05-25 21:43:50 -04:00
Deluan
fbd6c965b0 Always return public attribute in playlist response 2020-05-25 21:00:05 -04:00
Deluan
aaa4f1531e Ignore brackets in search 2020-05-25 11:05:30 -04:00
Deluan
72e92c7318 Fix nil pointer dereference 2020-05-25 10:54:07 -04:00
Deluan
72cb3850d1 Update React Admin to 3.5.3 2020-05-24 23:32:36 -04:00
647 changed files with 63075 additions and 16956 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.16",
// 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

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

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: deluan
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
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
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

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

@@ -6,7 +6,7 @@ ARG TARGETPLATFORM
RUN echo "Target Platform = ${TARGETPLATFORM}"
COPY dist .
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then cp navidrome_linux_musl_amd64_linux_amd64/navidrome /navidrome; fi
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then cp navidrome_linux_amd64_linux_amd64/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,11 +27,8 @@ COPY --from=copy-binary /navidrome /app/
VOLUME ["/data", "/music"]
ENV ND_MUSICFOLDER /music
ENV ND_DATAFOLDER /data
ENV ND_SCANINTERVAL 1m
ENV ND_TRANSCODINGCACHESIZE 100MB
ENV ND_SESSIONTIMEOUT 30m
ENV ND_LOGLEVEL info
ENV ND_PORT 4533
ENV GODEBUG "asyncpreemptoff=1"
EXPOSE ${ND_PORT}
HEALTHCHECK CMD wget -O- http://localhost:${ND_PORT}/ping || exit 1

View File

@@ -13,56 +13,66 @@ jobs:
name: Lint Server
runs-on: ubuntu-latest
steps:
- name: Install taglib
run: sudo apt-get install libtag1-dev
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v1
uses: golangci/golangci-lint-action@v2
with:
version: v1.27
version: v1.40
github-token: ${{ secrets.GITHUB_TOKEN }}
args: --timeout 2m
go:
name: Test Server on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
name: Test Server 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]
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
with:
node-version: 13
node-version: 16
- uses: actions/cache@v1
- uses: actions/cache@v2
id: cache-npm
with:
path: ~/.npm
@@ -75,20 +85,26 @@ jobs:
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
@@ -97,18 +113,23 @@ jobs:
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.3-0
uses: docker://deluan/ci-goreleaser:1.16.4-1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -116,16 +137,20 @@ jobs:
- name: Run GoReleaser - RELEASE
if: startsWith(github.ref, 'refs/tags/')
uses: docker://deluan/ci-goreleaser:1.14.3-0
uses: docker://deluan/ci-goreleaser:1.16.4-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
@@ -134,18 +159,20 @@ jobs:
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 +182,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/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

6
.gitignore vendored
View File

@@ -1,5 +1,6 @@
.DS_Store
.idea
.vscode
.envrc
/navidrome
/iTunes*.xml
@@ -11,14 +12,13 @@ TODO.md
var
navidrome.toml
master.zip
Jamstash-master
testDB
navidrome.db
*.swp
*_gen.go
embedded_gen.go
dist
music
docker-compose.override.yml
docker-compose.yml
navidrome.db-shm
navidrome.db-wal
tags

View File

@@ -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:
- -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,103 +10,107 @@ builds:
goarch:
- amd64
flags:
- -tags=embed
- -tags=embed,netgo
ldflags:
- "-extldflags '-static'"
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
- "-extldflags '-static -lz'"
- -s -w -X github.com/navidrome/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/navidrome/navidrome/consts.gitTag={{.Version}}
- id: navidrome_linux_musl_amd64
- id: navidrome_linux_386
env:
- CGO_ENABLED=1
- CC=musl-gcc
goos:
- linux
goarch:
- amd64
- 386
flags:
- -tags=embed
- -tags=embed,netgo
ldflags:
- "-extldflags '-static'"
- -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
- CXX=arm-linux-gnueabi-g++
goos:
- linux
goarch:
- arm
goarm:
- 5
- 6
- 7
flags:
- -tags=embed
- -tags=embed,netgo
ldflags:
- "-extldflags '-static'"
- -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
- CXX=aarch64-linux-gnu-g++
goos:
- linux
goarch:
- arm64
flags:
- -tags=embed
- -tags=embed,netgo
ldflags:
- "-extldflags '-static'"
- -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'"
- -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'"
- -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:
- id: musl
builds:
- navidrome_linux_musl_amd64
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_musl_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
replacements:
linux: Linux
amd64: x86_64
- id: default
builds:
- navidrome_darwin
- navidrome_linux_amd64
- navidrome_linux_arm
- navidrome_linux_arm64
- navidrome_windows_i686
- navidrome_windows_x64
format_overrides:
- format_overrides:
- goos: windows
format: zip
replacements:

2
.nvmrc
View File

@@ -1 +1 @@
v13
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)
```

View File

@@ -1,66 +0,0 @@
#####################################################
### Build UI bundles
FROM node:13-alpine AS jsbuilder
WORKDIR /src
COPY ui/package.json ui/package-lock.json ./
RUN npm ci
COPY ui/ .
RUN npm run build
#####################################################
### Build executable
FROM golang:1.14-alpine AS gobuilder
# Download build tools
RUN mkdir -p /src/ui/build
RUN apk add -U --no-cache build-base git
RUN go get -u github.com/go-bindata/go-bindata/...
# Download project dependencies
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
# Copy source, test it
COPY . .
RUN go test ./...
# Copy UI bundle, build executable
COPY --from=jsbuilder /src/build/* /src/ui/build/
COPY --from=jsbuilder /src/build/static/css/* /src/ui/build/static/css/
COPY --from=jsbuilder /src/build/static/js/* /src/ui/build/static/js/
RUN rm -rf /src/build/css /src/build/js
RUN GIT_TAG=$(git describe --tags `git rev-list --tags --max-count=1`) && \
GIT_TAG=${GIT_TAG#"tags/"} && \
GIT_SHA=$(git rev-parse --short HEAD) && \
echo "Building version: ${GIT_TAG} (${GIT_SHA})" && \
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}" -tags=embed
#####################################################
### Build Final Image
FROM alpine as release
LABEL maintainer="deluan@navidrome.org"
COPY --from=gobuilder /src/navidrome /app/
# Install ffmpeg and output build config
RUN apk add --no-cache ffmpeg
RUN ffmpeg -buildconf
VOLUME ["/data", "/music"]
ENV ND_MUSICFOLDER /music
ENV ND_DATAFOLDER /data
ENV ND_SCANINTERVAL 1m
ENV ND_TRANSCODINGCACHESIZE 100MB
ENV ND_SESSIONTIMEOUT 30m
ENV ND_LOGLEVEL info
ENV ND_PORT 4533
EXPOSE ${ND_PORT}
HEALTHCHECK CMD wget -O- http://localhost:${ND_PORT}/ping || exit 1
WORKDIR /app
ENTRYPOINT ["/app/navidrome"]

199
Makefile
View File

@@ -1,103 +1,144 @@
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)
GIT_SHA=$(shell git rev-parse --short HEAD)
GIT_TAG=$(shell git describe --tags `git rev-list --tags --max-count=1`)
## 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.16.4-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
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 ##@1_Run_First Install dependencies and prepare development environment
@echo Downloading Node dependencies...
@(cd ./ui && npm ci)
.PHONY: setup
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
@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 lefthook || (echo "Installing Lefthook" && GO111MODULE=off go get -u github.com/Arkweid/lefthook)
@lefthook install
.PHONY: setup
.PHONY: setup-dev
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
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/* .)
.PHONY: setup-git
check_env: check_go_env check_node_env
.PHONE: check_env
buildall: buildjs build ##@Build Build the project, both frontend and backend
.PHONY: buildall
check_hooks:
@lefthook add pre-commit
@lefthook add pre-push
.PHONE: check_hooks
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)
.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)
.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"
build: check_go_env ##@Build Build only backend
go build -ldflags="-X github.com/navidrome/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/navidrome/navidrome/consts.gitTag=$(GIT_TAG)-SNAPSHOT" -tags=netgo
.PHONY: build
buildall: check_env
buildjs: check_node_env ##@Build Build only frontend
@(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
.PHONY: buildjs
all: ##@Cross_Compilation Build binaries for all supported platforms. It does not build the frontend
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
@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 test
make pre-push
git tag v${V}
git push origin v${V}
git push origin v${V} --no-verify
.PHONY: release
snapshot:
docker run -it -v $(PWD):/workspace -w /workspace deluan/ci-goreleaser:1.14.3-0 goreleaser release --rm-dist --skip-publish --snapshot
.PHONY: snapshot
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)
@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)
@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
pre-push: lintall testall
.PHONY: pre-push
.DEFAULT_GOAL := help
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; \
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,24 +1,48 @@
# Navidrome Music Streamer
# Navidrome Music Server
[![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)
[![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/)
[![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://github.com/navidrome/navidrome/actions)
[![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).
## Installation
See instructions in the [project's website](https://www.navidrome.org/docs/installation/)
## Features
- Handles very **large music collections**
- Streams virtually **any audio format** available
- Reads and uses all your beautifully curated **metadata**
- 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 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)
- **Transcoding** on the fly. Can be set per user/player. **Opus encoding is supported**
- Translated to **various languages**
## Documentation
All documentation can be found in the project's homepage: https://www.navidrome.org/docs.
All documentation can be found in the project's website: https://www.navidrome.org/docs.
Here are some useful direct links:
- [Overview](https://www.navidrome.org/docs/overview/)
@@ -32,8 +56,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")
}

197
cmd/root.go Normal file
View File

@@ -0,0 +1,197 @@
package cmd
import (
"context"
"fmt"
"os"
"time"
"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"
)
var (
cfgFile string
noBanner bool
rootCmd = &cobra.Command{
Use: "navidrome",
Short: "Navidrome is a self-hosted music server and streamer",
Long: `Navidrome is a self-hosted music server and streamer.
Complete documentation is available at https://www.navidrome.org/docs`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
preRun()
},
Run: func(cmd *cobra.Command, args []string) {
runNavidrome()
},
Version: consts.Version(),
}
)
func Execute() {
rootCmd.SetVersionTemplate(`{{println .Version}}`)
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func preRun() {
if !noBanner {
println(consts.Banner())
}
conf.Load()
}
func runNavidrome() {
db.EnsureLatestVersion()
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", "schedule", schedule)
}
if err := g.Run(); err != nil {
log.Error("Fatal error in Navidrome. Aborting", err)
}
}
func startServer() (func() error, func(err error)) {
return func() error {
a := CreateServer(conf.Server.MusicFolder)
a.MountRouter("Subsonic API", consts.URLPathSubsonicAPI, CreateSubsonicAPIRouter())
a.MountRouter("Native API", consts.URLPathNativeAPI, CreateNativeAPIRouter())
if conf.Server.DevEnableScrobble {
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.Info("Executing initial scan")
if err := scanner.RescanAll(context.Background(), false); err != nil {
log.Error("Error executing initial scan", err)
}
}
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)
})
rootCmd.PersistentFlags().StringVarP(&cfgFile, "configfile", "c", "", `config file (default "./navidrome.toml")`)
rootCmd.PersistentFlags().BoolVarP(&noBanner, "nobanner", "n", false, `don't show banner`)
rootCmd.PersistentFlags().String("musicfolder", viper.GetString("musicfolder"), "folder where your music is stored")
rootCmd.PersistentFlags().String("datafolder", viper.GetString("datafolder"), "folder to store application data (DB, cache...), needs write access")
rootCmd.PersistentFlags().StringP("loglevel", "l", viper.GetString("loglevel"), "log level, possible values: error, info, debug, trace")
_ = viper.BindPFlag("musicfolder", rootCmd.PersistentFlags().Lookup("musicfolder"))
_ = viper.BindPFlag("datafolder", rootCmd.PersistentFlags().Lookup("datafolder"))
_ = viper.BindPFlag("loglevel", rootCmd.PersistentFlags().Lookup("loglevel"))
rootCmd.Flags().StringP("address", "a", viper.GetString("address"), "IP address to bind")
rootCmd.Flags().IntP("port", "p", viper.GetInt("port"), "HTTP port Navidrome will use")
rootCmd.Flags().Duration("sessiontimeout", viper.GetDuration("sessiontimeout"), "how long Navidrome will wait before closing web ui idle sessions")
rootCmd.Flags().Duration("scaninterval", viper.GetDuration("scaninterval"), "how frequently to scan for changes in your music library")
rootCmd.Flags().String("baseurl", viper.GetString("baseurl"), "base URL (only the path part) to configure Navidrome behind a proxy (ex: /music)")
rootCmd.Flags().String("uiloginbackgroundurl", viper.GetString("uiloginbackgroundurl"), "URL to a backaground image used in the Login page")
rootCmd.Flags().Bool("enabletranscodingconfig", viper.GetBool("enabletranscodingconfig"), "enables transcoding configuration in the UI")
rootCmd.Flags().String("transcodingcachesize", viper.GetString("transcodingcachesize"), "size of transcoding cache")
rootCmd.Flags().String("imagecachesize", viper.GetString("imagecachesize"), "size of image (art work) cache. set to 0 to disable cache")
rootCmd.Flags().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"))
_ = viper.BindPFlag("sessiontimeout", rootCmd.Flags().Lookup("sessiontimeout"))
_ = viper.BindPFlag("scaninterval", rootCmd.Flags().Lookup("scaninterval"))
_ = viper.BindPFlag("baseurl", rootCmd.Flags().Lookup("baseurl"))
_ = viper.BindPFlag("uiloginbackgroundurl", rootCmd.Flags().Lookup("uiloginbackgroundurl"))
_ = viper.BindPFlag("enabletranscodingconfig", rootCmd.Flags().Lookup("enabletranscodingconfig"))
_ = viper.BindPFlag("transcodingcachesize", rootCmd.Flags().Lookup("transcodingcachesize"))
_ = viper.BindPFlag("imagecachesize", rootCmd.Flags().Lookup("imagecachesize"))
}

36
cmd/scan.go Normal file
View File

@@ -0,0 +1,36 @@
package cmd
import (
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
var fullRescan bool
func init() {
scanCmd.Flags().BoolVarP(&fullRescan, "full", "f", false, "check all subfolders, ignoring timestamps")
rootCmd.AddCommand(scanCmd)
}
var scanCmd = &cobra.Command{
Use: "scan",
Short: "Scan music folder",
Long: "Scan music folder for updates",
Run: func(cmd *cobra.Command, args []string) {
runScanner()
},
}
func runScanner() {
conf.Server.DevPreCacheAlbumArtwork = false
scanner := GetScanner()
_ = scanner.RescanAll(context.Background(), fullRescan)
if fullRescan {
log.Info("Finished full rescan")
} else {
log.Info("Finished rescan")
}
}

17
cmd/signaler_unix.go Normal file
View File

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

89
cmd/wire_gen.go Normal file
View File

@@ -0,0 +1,89 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//+build !wireinject
package cmd
import (
"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/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()
serverServer := server.New(dataStore)
return serverServer
}
func CreateNativeAPIRouter() *nativeapi.Router {
dataStore := persistence.New()
broker := events.GetBroker()
share := core.NewShare(dataStore)
router := nativeapi.New(dataStore, broker, share)
return router
}
func CreateSubsonicAPIRouter() *subsonic.Router {
dataStore := persistence.New()
artworkCache := core.GetImageCache()
artwork := core.NewArtwork(dataStore, artworkCache)
transcoderTranscoder := transcoder.New()
transcodingCache := core.GetTranscodingCache()
mediaStreamer := core.NewMediaStreamer(dataStore, transcoderTranscoder, transcodingCache)
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 {
dataStore := persistence.New()
router := lastfm.NewRouter(dataStore)
return router
}
func createScanner() scanner.Scanner {
dataStore := persistence.New()
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(core.Set, subsonic.New, nativeapi.New, persistence.New, lastfm.NewRouter, events.GetBroker)
// Scanner must be a Singleton
var (
onceScanner sync.Once
scannerInstance scanner.Scanner
)
func GetScanner() scanner.Scanner {
onceScanner.Do(func() {
scannerInstance = createScanner()
})
return scannerInstance
}

73
cmd/wire_injectors.go Normal file
View File

@@ -0,0 +1,73 @@
//+build wireinject
package cmd
import (
"sync"
"github.com/navidrome/navidrome/server/events"
"github.com/google/wire"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/agents/lastfm"
"github.com/navidrome/navidrome/persistence"
"github.com/navidrome/navidrome/scanner"
"github.com/navidrome/navidrome/server"
"github.com/navidrome/navidrome/server/nativeapi"
"github.com/navidrome/navidrome/server/subsonic"
)
var allProviders = wire.NewSet(
core.Set,
subsonic.New,
nativeapi.New,
persistence.New,
lastfm.NewRouter,
events.GetBroker,
)
func CreateServer(musicFolder string) *server.Server {
panic(wire.Build(
server.New,
allProviders,
))
}
func CreateNativeAPIRouter() *nativeapi.Router {
panic(wire.Build(
allProviders,
))
}
func CreateSubsonicAPIRouter() *subsonic.Router {
panic(wire.Build(
allProviders,
GetScanner,
))
}
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

@@ -1,125 +1,265 @@
package conf
import (
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/log"
"github.com/koding/multiconfig"
"github.com/kr/pretty"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log"
"github.com/robfig/cron/v3"
"github.com/spf13/viper"
)
type nd struct {
ConfigFile string `default:"./navidrome.toml"`
Port string `default:"4533"`
MusicFolder string `default:"./music"`
DataFolder string `default:"./"`
ScanInterval string `default:"1m"`
DbPath string ``
LogLevel string `default:"info"`
SessionTimeout string `default:"30m"`
BaseURL string `default:""`
type configOptions struct {
ConfigFile string
Address string
Port int
MusicFolder string
DataFolder string
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
UILoginBackgroundURL string `default:"https://source.unsplash.com/random/1600x900?music"`
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
IgnoredArticles string `default:"The El La Los Las Le Les Os As O A"`
IndexGroups string `default:"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]([)"`
Scanner scannerOptions
EnableTranscodingConfig bool `default:"false"`
TranscodingCacheSize string `default:"100MB"` // in MB
ImageCacheSize string `default:"100MB"` // in MB
ProbeCommand string `default:"ffmpeg %s -f ffmetadata"`
Agents string
LastFM lastfmOptions
Spotify spotifyOptions
// DevFlags. These are used to enable/disable debugging and incomplete features
DevLogSourceLine bool `default:"false"`
DevAutoCreateAdminPassword string `default:""`
DevEnableUIPlaylists bool `default:"true"`
DevEnableUIStarred bool `default:"true"`
DevLogSourceLine bool
DevAutoCreateAdminPassword string
DevAutoLoginUsername string
DevPreCacheAlbumArtwork bool
DevFastAccessCoverArt bool
DevOldCacheLayout bool
DevActivityPanel bool
DevEnableShare bool
DevEnableScrobble bool
}
var Server = &nd{}
// TODO refactor configuration and use something different. Maybe https://github.com/spf13/cobra
// This function loads the config just load the ConfigFile. This is very cumbersome, but doesn't
// seem there's a simpler way to do thiswith multiconfig. Time to replace this library?
func configFile() string {
conf := &nd{}
loader := multiconfig.MultiLoader(
&multiconfig.TagLoader{},
&multiconfig.EnvironmentLoader{},
&multiconfig.FlagLoader{},
)
d := &multiconfig.DefaultLoader{}
d.Loader = loader
d.Validator = multiconfig.MultiValidator(&multiconfig.RequiredValidator{})
if err := d.Load(conf); err != nil {
return consts.LocalConfigFile
}
if _, err := os.Stat(conf.ConfigFile); err != nil {
return consts.LocalConfigFile
}
return conf.ConfigFile
type scannerOptions struct {
Extractor string
}
func newWithPath(path string, skipFlags ...bool) *multiconfig.DefaultLoader {
var loaders []multiconfig.Loader
// Read default values defined via tag fields "default"
loaders = append(loaders, &multiconfig.TagLoader{})
if _, err := os.Stat(path); err == nil {
if strings.HasSuffix(path, "toml") {
loaders = append(loaders, &multiconfig.TOMLLoader{Path: path})
}
if strings.HasSuffix(path, "json") {
loaders = append(loaders, &multiconfig.JSONLoader{Path: path})
}
if strings.HasSuffix(path, "yml") || strings.HasSuffix(path, "yaml") {
loaders = append(loaders, &multiconfig.YAMLLoader{Path: path})
}
}
e := &multiconfig.EnvironmentLoader{}
loaders = append(loaders, e)
if len(skipFlags) == 0 || !skipFlags[0] {
f := &multiconfig.FlagLoader{}
loaders = append(loaders, f)
}
loader := multiconfig.MultiLoader(loaders...)
d := &multiconfig.DefaultLoader{}
d.Loader = loader
d.Validator = multiconfig.MultiValidator(&multiconfig.RequiredValidator{})
return d
type lastfmOptions struct {
Enabled bool
ApiKey string
Secret string
Language string
}
func LoadFromFile(confFile string, skipFlags ...bool) {
m := newWithPath(confFile, skipFlags...)
err := m.Load(Server)
if err == flag.ErrHelp {
os.Exit(1)
}
if err != nil {
fmt.Printf("Error trying to load config '%s'. Error: %v", confFile, err)
os.Exit(2)
}
if Server.DbPath == "" {
Server.DbPath = filepath.Join(Server.DataFolder, consts.DefaultDbPath)
}
if os.Getenv("PORT") != "" {
Server.Port = os.Getenv("PORT")
}
log.SetLevelString(Server.LogLevel)
log.SetLogSourceLine(Server.DevLogSourceLine)
log.Debug("Loaded configuration", "file", confFile, "config", fmt.Sprintf("%#v", Server))
type spotifyOptions struct {
ID string
Secret string
}
var (
Server = &configOptions{}
hooks []func()
)
func LoadFromFile(confFile string) {
viper.SetConfigFile(confFile)
Load()
}
func Load() {
LoadFromFile(configFile())
err := viper.Unmarshal(&Server)
if err != nil {
fmt.Println("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)
os.Exit(1)
}
Server.ConfigFile = viper.GetViper().ConfigFileUsed()
if Server.DbPath == "" {
Server.DbPath = filepath.Join(Server.DataFolder, consts.DefaultDbPath)
}
log.SetLevelString(Server.LogLevel)
log.SetLogSourceLine(Server.DevLogSourceLine)
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 == "" {
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", 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", -1)
viper.SetDefault("scanschedule", "@every 1m")
viper.SetDefault("baseurl", "")
viper.SetDefault("uiloginbackgroundurl", consts.DefaultUILoginBackgroundURL)
viper.SetDefault("enabletranscodingconfig", false)
viper.SetDefault("transcodingcachesize", "100MB")
viper.SetDefault("imagecachesize", "100MB")
viper.SetDefault("autoimportplaylists", true)
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", "taglib")
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("devautologinusername", "")
viper.SetDefault("devprecachealbumartwork", false)
viper.SetDefault("devoldcachelayout", false)
viper.SetDefault("devfastaccesscoverart", false)
viper.SetDefault("devactivitypanel", true)
viper.SetDefault("devenableshare", false)
viper.SetDefault("devenablescrobble", true)
}
func InitConfig(cfgFile string) {
cfgFile = getConfigFile(cfgFile)
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Search config in local directory with name "navidrome" (without extension).
viper.AddConfigPath(".")
viper.SetConfigName("navidrome")
}
_ = 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)
os.Exit(1)
}
}
func getConfigFile(cfgFile string) string {
if cfgFile != "" {
return cfgFile
}
return os.Getenv("ND_CONFIGFILE")
}

View File

@@ -5,7 +5,7 @@ import (
"strings"
"unicode"
"github.com/deluan/navidrome/resources"
"github.com/navidrome/navidrome/resources"
)
func getBanner() string {

View File

@@ -10,26 +10,43 @@ import (
const (
AppName = "navidrome"
LocalConfigFile = "./navidrome.toml"
DefaultDbPath = "navidrome.db?cache=shared&_busy_timeout=15000&_journal_mode=WAL"
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 = 30 * time.Minute
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
)
// Cache options
@@ -44,6 +61,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{}{
{
@@ -54,7 +77,7 @@ 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 -",
},
@@ -65,4 +88,6 @@ var (
VariousArtists = "Various Artists"
VariousArtistsID = fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(VariousArtists))))
UnknownArtist = "[Unknown Artist]"
ServerStart = time.Now()
)

View File

@@ -2,6 +2,10 @@ package consts
import "mime"
var LosslessFormats = []string{
"flac", "wav", "alac", "ape", "dsf", "wav", "shn", "wv", "wvp",
}
func init() {
mt := map[string]string{
".mp3": "audio/mpeg",
@@ -9,6 +13,7 @@ func init() {
".oga": "audio/ogg",
".opus": "audio/ogg",
".aac": "audio/mp4",
".alac": "audio/mp4",
".m4a": "audio/mp4",
".m4b": "audio/mp4",
".flac": "audio/flac",
@@ -19,9 +24,15 @@ func init() {
".shn": "audio/x-shn",
".aif": "audio/x-aiff",
".aiff": "audio/x-aiff",
".m3u": "audio/x-mpegurl",
".pls": "audio/x-scpls",
".dsf": "audio/dsd",
".wv": "audio/x-wavpack",
".wvp": "audio/x-wavpack",
".gif": "image/gif",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".webp": "image/webp",
".png": "image/png",
".bmp": "image/bmp",
}

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
}

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

@@ -0,0 +1,226 @@
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 {
return err
}
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 {
return err
}
return nil
}
func (l *lastfmAgent) Scrobble(ctx context.Context, userId string, scrobbles []scrobbler.Scrobble) error {
sk, err := l.sessionKeys.get(ctx, userId)
if err != nil {
return err
}
// TODO Implement batch scrobbling
for _, s := range scrobbles {
if s.Duration <= 30 {
log.Debug(ctx, "Skipping Last.fm scrobble for short song", "track", s.Title, "duration", s.Duration)
continue
}
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 err
}
}
return nil
}
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,305 @@
package lastfm
import (
"bytes"
"context"
"errors"
"io/ioutil"
"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: ioutil.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: ioutil.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: ioutil.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: ioutil.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: ioutil.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: ioutil.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: ioutil.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: ioutil.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: ioutil.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: ioutil.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))
})
})
Describe("Scrobble", func() {
It("calls Last.fm with correct params", func() {
ts := time.Now()
scrobbles := []scrobbler.Scrobble{{MediaFile: *track, TimeStamp: ts}}
httpClient.Res = http.Response{Body: ioutil.NopCloser(bytes.NewBufferString("{}")), StatusCode: 200}
err := agent.Scrobble(ctx, "user-1", scrobbles)
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
scrobbles := []scrobbler.Scrobble{{MediaFile: *track, TimeStamp: time.Now()}}
httpClient.Res = http.Response{Body: ioutil.NopCloser(bytes.NewBufferString("{}")), StatusCode: 200}
err := agent.Scrobble(ctx, "user-1", scrobbles)
Expect(err).ToNot(HaveOccurred())
Expect(httpClient.SavedRequest).To(BeNil())
})
})
})
})

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/ioutil"
"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: ioutil.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: ioutil.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: ioutil.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: ioutil.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: ioutil.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: ioutil.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,124 @@
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"`
Streamable string `json:"streamable"`
Stats struct {
Listeners string `json:"listeners"`
Plays string `json:"plays"`
} `json:"stats"`
Similar SimilarArtists `json:"similar"`
Tags struct {
Tag []ArtistTag `json:"tag"`
} `json:"tags"`
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 ArtistTag struct {
Name string `json:"name"`
URL string `json:"url"`
}
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,70 @@
package lastfm
import (
"encoding/json"
"io/ioutil"
. "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, _ := ioutil.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"))
similarArtists := []string{"Passengers", "INXS", "R.E.M.", "Simple Minds", "Bono"}
for i, similar := range similarArtists {
Expect(resp.Artist.Similar.Artists[i].Name).To(Equal(similar))
}
})
})
Describe("SimilarArtists", func() {
It("parses the response correctly", func() {
var resp Response
body, _ := ioutil.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, _ := ioutil.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/ioutil"
"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 := ioutil.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/ioutil"
"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: ioutil.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: ioutil.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: ioutil.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: ioutil.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: ioutil.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: ioutil.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: ioutil.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"
"io/ioutil"
. "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, _ := ioutil.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")
}

110
core/archiver.go Normal file
View File

@@ -0,0 +1,110 @@
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).FindByAlbum(id)
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
}

221
core/artwork.go Normal file
View File

@@ -0,0 +1,221 @@
package core
import (
"bytes"
"context"
"errors"
"fmt"
"image"
_ "image/gif"
"image/jpeg"
_ "image/png"
"io"
"io/ioutil"
"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.Assets().Open(consts.PlaceholderAlbumArt)
}
}()
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)
}
return
}
func resizeImage(reader io.Reader, size int) (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)
err = jpeg.Encode(buf, m, &jpeg.Options{Quality: conf.Server.CoverJpegQuality})
return ioutil.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 ioutil.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"
"io/ioutil"
"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, _ = ioutil.TempDir("", "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!"))
})
})
})
})

67
core/auth/auth.go Normal file
View File

@@ -0,0 +1,67 @@
package auth
import (
"context"
"sync"
"time"
"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
Secret []byte
TokenAuth *jwtauth.JWTAuth
)
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)
}
Secret = []byte(secret)
TokenAuth = jwtauth.New("HS256", Secret, nil)
})
}
func CreateToken(u *model.User) (string, error) {
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 TouchToken(token jwt.Token) (string, error) {
claims, err := token.AsMap(context.Background())
if err != nil {
return "", err
}
claims[jwt.ExpirationKey] = time.Now().UTC().Add(conf.Server.SessionTimeout).Unix()
_, newToken, err := TokenAuth.Encode(claims)
return newToken, err
}
func Validate(tokenStr string) (map[string]interface{}, error) {
token, err := jwtauth.VerifyToken(TokenAuth, tokenStr)
if err != nil {
return nil, err
}
return token.AsMap(context.Background())
}

108
core/auth/auth_test.go Normal file
View File

@@ -0,0 +1,108 @@
package auth_test
import (
"testing"
"time"
"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"
)
func TestAuth(t *testing.T) {
log.SetLevel(log.LevelCritical)
RegisterFailHandler(Fail)
RunSpecs(t, "Auth Test Suite")
}
const (
testJWTSecret = "not so secret"
oneDay = 24 * time.Hour
)
var _ = Describe("Auth", func() {
BeforeSuite(func() {
conf.Server.SessionTimeout = 2 * oneDay
})
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(HaveOccurred())
})
It("returns the claims from a valid JWT token", func() {
claims := map[string]interface{}{}
claims["iss"] = "issuer"
claims["iat"] = time.Now().Unix()
claims["exp"] = time.Now().Add(1 * time.Minute).Unix()
_, tokenStr, err := auth.TokenAuth.Encode(claims)
Expect(err).NotTo(HaveOccurred())
decodedClaims, err := auth.Validate(tokenStr)
Expect(err).NotTo(HaveOccurred())
Expect(decodedClaims["iss"]).To(Equal("issuer"))
})
It("returns ErrExpired if the `exp` field is in the past", func() {
claims := map[string]interface{}{}
claims["iss"] = "issuer"
claims["exp"] = time.Now().Add(-1 * time.Minute).Unix()
_, tokenStr, err := auth.TokenAuth.Encode(claims)
Expect(err).NotTo(HaveOccurred())
_, 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))
})
})
})

88
core/cache_warmer.go Normal file
View File

@@ -0,0 +1,88 @@
package core
import (
"context"
"io"
"io/ioutil"
"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(ioutil.Discard, r)
}
type artworkItem struct {
albumID string
}

15
core/common.go Normal file
View File

@@ -0,0 +1,15 @@
package core
import (
"context"
"github.com/navidrome/navidrome/model/request"
)
func userName(ctx context.Context) string {
if user, ok := request.UserFrom(ctx); !ok {
return "UNKNOWN"
} else {
return user.UserName
}
}

View File

@@ -1,17 +1,17 @@
package app
package core
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"
)
func TestApp(t *testing.T) {
func TestCore(t *testing.T) {
tests.Init(t, false)
log.SetLevel(log.LevelCritical)
RegisterFailHandler(Fail)
RunSpecs(t, "RESTful API Suite")
RunSpecs(t, "Core Suite")
}

389
core/external_metadata.go Normal file
View File

@@ -0,0 +1,389 @@
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
}
}
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{"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{"id": ids},
})
if err != nil {
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
}

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

@@ -1,4 +1,4 @@
package engine
package core
import (
"context"
@@ -6,22 +6,23 @@ import (
"io"
"mime"
"os"
"sync"
"time"
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/engine/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}
@@ -30,7 +31,18 @@ func NewMediaStreamer(ds model.DataStore, ffm transcoder.Transcoder, cache Trans
type mediaStreamer struct {
ds model.DataStore
ffm transcoder.Transcoder
cache fscache.Cache
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.ffm.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

@@ -1,14 +1,16 @@
package engine
package core
import (
"context"
"io"
"io/ioutil"
"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, _ = ioutil.TempDir("", "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())
_, _ = ioutil.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,21 @@
package scrobbler
import (
"context"
"time"
"github.com/navidrome/navidrome/model"
)
type Scrobble struct {
model.MediaFile
TimeStamp time.Time
}
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, scrobbles []Scrobble) error
}
type Constructor func(ds model.DataStore) Scrobbler

View File

@@ -0,0 +1,193 @@
package scrobbler
import (
"context"
"sort"
"time"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/server/events"
"github.com/navidrome/navidrome/log"
"github.com/ReneKroon/ttlcache/v2"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"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
}
func GetPlayTracker(ds model.DataStore, broker events.Broker) PlayTracker {
instance := singleton.Get(playTracker{}, func() interface{} {
m := ttlcache.NewCache()
m.SkipTTLExtensionOnHit(true)
_ = m.SetTTL(nowPlayingExpire)
return &playTracker{ds: ds, playMap: m, broker: broker}
})
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, constructor := range scrobblers {
err := func() error {
s := constructor(p.ds)
if !s.IsAuthorized(ctx, userId) {
return nil
}
log.Debug(ctx, "Sending NowPlaying info", "scrobbler", name, "track", t.Title, "artist", t.Artist)
return 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)
scrobbles := []Scrobble{{MediaFile: *t, TimeStamp: playTime}}
// TODO Parallelize
for name, constructor := range scrobblers {
err := func() error {
s := constructor(p.ds)
if !s.IsAuthorized(ctx, u.ID) {
return nil
}
log.Debug(ctx, "Sending Scrobble", "scrobbler", name, "track", t.Title, "artist", t.Artist)
return s.Scrobble(ctx, u.ID, scrobbles)
}()
if err != nil {
log.Error(ctx, "Error sending Scrobble", "scrobbler", name, "track", t.Title, "artist", t.Artist, err)
return err
}
}
return nil
}
var scrobblers map[string]Constructor
func Register(name string, init Constructor) {
if !conf.Server.DevEnableScrobble {
return
}
if scrobblers == nil {
scrobblers = make(map[string]Constructor)
}
scrobblers[name] = init
}

View File

@@ -0,0 +1,206 @@
package scrobbler
import (
"context"
"errors"
"time"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/server/events"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"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 broker PlayTracker
var track model.MediaFile
var album model.Album
var artist model.Artist
var fake *fakeScrobbler
BeforeEach(func() {
conf.Server.DevEnableScrobble = true
ctx = context.Background()
ctx = request.WithUser(ctx, model.User{ID: "u-1"})
ctx = request.WithPlayer(ctx, model.Player{ScrobbleEnabled: true})
ds = &tests.MockDataStore{}
broker = GetPlayTracker(ds, events.GetBroker())
fake = &fakeScrobbler{Authorized: true}
Register("fake", func(ds model.DataStore) Scrobbler {
return fake
})
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 := broker.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 := broker.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 := broker.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"})
_ = broker.NowPlaying(ctx, "player-1", "player-one", "123")
ctx = request.WithUser(ctx, model.User{UserName: "user-2"})
_ = broker.NowPlaying(ctx, "player-2", "player-two", "456")
playing, err := broker.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 := broker.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.Scrobbles[0].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 := broker.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 := broker.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 := broker.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 := broker.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
Scrobbles []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, scrobbles []Scrobble) error {
f.ScrobbleCalled = true
if f.Error != nil {
return f.Error
}
f.UserID = userId
f.Scrobbles = scrobbles
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,11 +16,10 @@ type Transcoder interface {
}
func New() Transcoder {
path, err := exec.LookPath("ffmpeg")
_, err := exec.LookPath("ffmpeg")
if err != nil {
log.Error("Unable to find ffmpeg", err)
}
log.Debug("Found ffmpeg", "path", path)
return &ffmpeg{}
}

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

23
core/wire_providers.go Normal file
View File

@@ -0,0 +1,23 @@
package core
import (
"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(
NewArtwork,
NewMediaStreamer,
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/pressly/goose"
)
@@ -27,7 +28,7 @@ func Db() *sql.DB {
var err error
Path = conf.Server.DbPath
if Path == ":memory:" {
Path = "file::memory:?cache=shared"
Path = "file::memory:?cache=shared&_foreign_keys=on"
conf.Server.DbPath = Path
}
log.Debug("Opening DataBase", "dbPath", Path, "driver", Driver)
@@ -42,7 +43,22 @@ func Db() *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 +69,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"

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