Compare commits

...

788 Commits

Author SHA1 Message Date
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
Deluan
a6cc88177c Fix "starred" sorting 2020-05-24 12:49:32 -04:00
Deluan Quintão
d6ad833538 Update de.json (POEditor.com) 2020-05-24 12:23:39 -04:00
Deluan Quintão
eb1749ce71 Update fr.json (POEditor.com) 2020-05-24 12:23:39 -04:00
Deluan Quintão
acebe18c95 Update cs.json (POEditor.com) 2020-05-24 12:23:39 -04:00
Deluan
cac1a20ec8 Use a ☆ instead of the word "starred" 2020-05-24 12:14:45 -04:00
Deluan
ac8f92d7ac Fix ContextMenu column label 2020-05-24 12:14:44 -04:00
Deluan
207565bde0 Update pt translation 2020-05-24 11:33:05 -04:00
Deluan
3ae1586e10 Add "No playlists available" to context menu 2020-05-24 11:27:17 -04:00
Deluan
5c46f7822f Better disc subtitle 2020-05-23 15:33:29 -04:00
Deluan
c13766bbc3 More optimization for small screens 2020-05-23 14:11:39 -04:00
Deluan
290e8c4bf0 Make Starred into a "QuickFilter" 2020-05-23 13:25:30 -04:00
Deluan
442671578d Fix warning in JS console (wrong property type) 2020-05-23 12:49:39 -04:00
Deluan
1bca8fca97 Enable UI starred by default 2020-05-23 01:07:34 -04:00
Deluan
e811816021 Fix pagination in Songs when filtered by starred 2020-05-23 00:43:45 -04:00
Deluan
9331be67a3 Fix pagination in Songs 2020-05-23 00:17:35 -04:00
Deluan
55ad5c9fc9 Remove unused import, fix build 2020-05-22 23:33:40 -04:00
Deluan
ec0002e77a Add a sortable Starred column and a Starred filter to Song List 2020-05-22 23:10:58 -04:00
Deluan
3632608de0 Replace child.type.name, as it is not available in the production build 2020-05-22 22:23:00 -04:00
Deluan
0a3e6c66c1 Alwasy show context menu on mobile views 2020-05-22 21:47:48 -04:00
Deluan
52a46e61e0 Remove duplication 2020-05-22 21:31:45 -04:00
Deluan
de2759b3d5 Fix react key conflic 2020-05-22 20:48:49 -04:00
Deluan
978e7f2eaa Only show SongContextMenu on hover 2020-05-22 20:15:58 -04:00
delucks
ae847103a2 Correct response body for getSongsByGenre 2020-05-22 18:08:35 -04:00
Deluan
6f6b223453 Disable ToggleStar on playlist tracks 2020-05-22 15:45:03 -04:00
Deluan
8a68cecdb9 Add ToggleStar to SongContextMenu (WIP) 2020-05-22 15:23:42 -04:00
Deluan
e21262675e More log to media_streamer 2020-05-21 21:26:48 -04:00
Deluan
a3ba05b2cc Use latest ci-goreleaser: Go 1.14.3 and Goreleaser 1.35.0 2020-05-21 14:56:56 -04:00
Deluan
294712739a Bump ginkgo/gomega dependencies 2020-05-21 13:41:12 -04:00
Fup
ad725ac355 Add [Install] section to systemd unit file.
This section apparently is mandatory now.
2020-05-21 08:19:12 -04:00
Deluan
17df63b550 Fix child.size and directory.playCount compatibility with Subsonic API. Fixes #304 2020-05-19 23:51:23 -04:00
Deluan
c2d1e9df9f Remove orphan tracks from playlists after they are removed from library 2020-05-18 20:32:01 -04:00
Deluan
0e4f7036eb Make playlist songs look better in mobile 2020-05-18 18:00:55 -04:00
Deluan
a4183aea8c Unexport private functions 2020-05-18 15:06:33 -04:00
Deluan
9e845cb116 Skip scanning folders if they contain a .ndignore file. Closes #297 2020-05-18 14:37:01 -04:00
Deluan Quintão
f82fefe0ab Update de.json (POEditor.com) 2020-05-18 13:52:30 -04:00
Deluan
f28531b609 Add album name to song details 2020-05-18 13:32:12 -04:00
Deluan
14f3ffbee6 Allow sorting playlist tracks 2020-05-18 13:21:47 -04:00
Deluan
94e1b1f65d Add context menu to playlist songs 2020-05-18 13:05:54 -04:00
Deluan
274eb805f9 Upgrade golangci-lint 2020-05-18 12:43:03 -04:00
Deluan
84ea852339 Prettier 2020-05-18 12:19:01 -04:00
Deluan
cf019849f0 Add missing translation key 2020-05-18 12:15:46 -04:00
Deluan
76a5d1928e Fix some JS warnings 2020-05-18 12:12:04 -04:00
Deluan
3dced978c7 Add button to edit playlist details 2020-05-18 12:12:04 -04:00
Deluan
6071ae143e Bump ginkgo version 2020-05-18 10:25:33 -04:00
Deluan
05a07f31c9 Bump react-music-player version 2020-05-18 10:14:50 -04:00
Deluan
1afbbbf189 Add SongContextMenu to Album Songs 2020-05-17 20:57:38 -04:00
Deluan
308163c2e0 Add "AddToPlaylist" to AlbumContextMenu 2020-05-17 20:30:05 -04:00
Deluan Quintão
176bfe1506 Update fr.json (POEditor.com) 2020-05-17 15:51:52 -04:00
Deluan Quintão
4c3f3f3573 Update cs.json (POEditor.com) 2020-05-17 15:51:50 -04:00
Deluan
1aef21a4a9 Update translations for playlists 2020-05-17 10:23:46 -04:00
Deluan
d1a0ffaaee Check permissions in playlists 2020-05-16 23:14:28 -04:00
Deluan
41010515ee Enable Playlist Management in the UI by default 2020-05-16 19:16:48 -04:00
Deluan
a734a1aaa3 Add filter by name to Playlist list 2020-05-16 19:14:19 -04:00
Deluan
bf1dc33782 Add option to shuffle playlist 2020-05-16 19:11:52 -04:00
Deluan
c43798c5dd Filter out songs not in the playlist 2020-05-16 19:02:33 -04:00
Deluan
12cf2f1104 Remove tracks from playlist 2020-05-16 18:35:34 -04:00
Deluan
5c95eed517 Rename actions 2020-05-16 18:35:34 -04:00
Deluan
e81a9dd1b5 Add tracks to playlist 2020-05-16 18:35:34 -04:00
Deluan
fd49ae319f Add Playlist action 2020-05-16 18:35:34 -04:00
Deluan
f881e2a54b Add option to enable (experimental) playlists in UI 2020-05-16 18:35:34 -04:00
Deluan
0ca79eead4 Show Playlist tracks 2020-05-16 18:35:34 -04:00
Deluan
8a709c489a Add playlist views 2020-05-16 18:35:34 -04:00
Deluan
b1f5d35f73 Fix DurationField breaking when the record does not have the source field 2020-05-16 18:35:34 -04:00
Deluan
5682d0e721 Remove tracks from Playlist's GetAll 2020-05-16 18:35:34 -04:00
Deluan
ab690215ef Make Playlist's songCount sortable 2020-05-16 18:35:34 -04:00
Deluan
8f9601090c Add helper functions tests 2020-05-16 18:35:34 -04:00
Deluan
aebee651ac Add nested resource playlist/{id}/tracks 2020-05-16 18:35:34 -04:00
Deluan
a56e588c8e Create relation table for playlist tracks 2020-05-16 18:35:34 -04:00
Deluan
27de18f8c9 Fix typo 2020-05-16 18:35:34 -04:00
Deluan
5afcd0ad22 Make field names camelCase 2020-05-16 18:35:34 -04:00
Deluan
fec589dce5 Add playlist list 2020-05-16 18:35:34 -04:00
Deluan
4e613be960 Add playlists REST endpoint 2020-05-16 18:35:34 -04:00
Deluan
8e2480a82d Fix duration (in web player) when playing transcoded files. Thanks @lijinke666
See: https://github.com/lijinke666/react-music-player/issues/90
2020-05-16 13:24:25 -04:00
Deluan
50eda78ca1 Revert "Save perPage selection in localstorage"
This reverts commit 9490374faa.
2020-05-15 11:04:48 -04:00
Deluan
b3af0f880b Use a custom List component 2020-05-15 11:03:59 -04:00
Deluan
9490374faa Save perPage selection in localstorage 2020-05-14 23:05:14 -04:00
Deluan
a340b62fdf Link to artist from album list 2020-05-14 20:42:21 -04:00
Deluan
0d1af8c635 Fix potential null reference exception 2020-05-14 19:01:07 -04:00
Deluan
377c9e6be6 Revert "Upgrade golangci-lint"
This reverts commit b8ae5ccb02.
2020-05-13 23:26:54 -04:00
Deluan
b8ae5ccb02 Upgrade golangci-lint 2020-05-13 16:50:13 -04:00
Deluan
f8362a4acb Fix staticcheck's SA1029 2020-05-13 16:49:55 -04:00
Deluan
5ce3135f00 Fix gosec's G601 2020-05-13 15:32:42 -04:00
Deluan
162971f7b3 Remove console.log 2020-05-12 14:48:22 -04:00
Deluan
49dd13002c Update Portuguese translation 2020-05-12 14:20:24 -04:00
Deluan
1e5c879fc6 Extract disc subtitle strings for translation 2020-05-12 14:13:34 -04:00
Deluan
e369cbf493 Disable album songs sorting 2020-05-12 13:47:59 -04:00
Deluan
a88270a22b Add multidisc labels, even if there are no disc subtitles 2020-05-12 13:14:23 -04:00
Deluan
4355f4fe2d Show disc subtitles (if available) 2020-05-12 12:57:53 -04:00
Deluan
0d9361734f Import and display disc subtitles 2020-05-12 12:57:53 -04:00
Deluan
7f75994906 go mod tidy 2020-05-11 10:54:35 -04:00
dependabot-preview[bot]
e9d594ebcf Bump github.com/Masterminds/squirrel from 1.3.0 to 1.4.0
Bumps [github.com/Masterminds/squirrel](https://github.com/Masterminds/squirrel) from 1.3.0 to 1.4.0.
- [Release notes](https://github.com/Masterminds/squirrel/releases)
- [Commits](https://github.com/Masterminds/squirrel/compare/v1.3.0...v1.4.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-11 09:46:22 -04:00
Deluan
0d1e2a92f6 Make MediaFolder ID int32 2020-05-09 22:29:02 -04:00
Deluan
1ed6d130b1 Fix getMusicFolders and getIndexes API compliance. Fix #286 2020-05-09 21:02:38 -04:00
Deluan
09267d2ffd Don't skip to the next song when there is an streaming error 2020-05-09 15:56:12 -04:00
Deluan
3a6639f820 Fix golangci-lint installation 2020-05-09 15:02:23 -04:00
Deluan Quintão
8b79b288eb Update de.json (POEditor.com) 2020-05-09 12:53:51 -04:00
Deluan Quintão
a0cde80c52 Update pt.json (POEditor.com) 2020-05-09 12:53:51 -04:00
Deluan Quintão
458636d2b8 Update fr.json (POEditor.com) 2020-05-09 12:53:51 -04:00
Deluan Quintão
8b30af561e Update cs.json (POEditor.com) 2020-05-09 12:53:51 -04:00
Deluan Quintão
1fb2b9bf1d Update tr.json (POEditor.com) (+1 squashed commit)
Squashed commits:
[9480cf8] Update tr.json (POEditor.com)
2020-05-09 12:53:51 -04:00
Deluan
5c9fdb064d Use official golangci-lint GH action 2020-05-08 17:13:57 -04:00
Deluan
70047fe20e Add songCount column to Artist table 2020-05-08 10:05:48 -04:00
Deluan
1c41582d79 Run pre-push hooks in parallel 2020-05-07 12:00:10 -04:00
Deluan
9a854f6cc4 Add golangci-lint to git pre-push hook 2020-05-07 11:57:07 -04:00
Deluan
06ab88415a Refactor album actions, simplify usage 2020-05-07 11:24:28 -04:00
Deluan
16f2b056ef Fix deprecated version input 2020-05-07 09:50:01 -04:00
Deluan
a761e6f2d0 go mod tidy 2020-05-07 09:31:10 -04:00
dependabot-preview[bot]
da7489cecd Bump github.com/onsi/gomega from 1.9.0 to 1.10.0
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.9.0...v1.10.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-07 09:12:17 -04:00
Deluan
0472988645 Fix next track not working after adding to queue 2020-05-07 00:28:32 -04:00
Deluan
7e0881f0ec Play the remainder of the album when clicking on a album's song 2020-05-06 16:02:31 -04:00
Deluan
f8fb4c8f54 Make the borders of the AlbumSongs round 2020-05-06 08:44:25 -04:00
Deluan
ddcacbb6e5 Fix covers overflow in some resolutions 2020-05-06 08:44:10 -04:00
Deluan Quintão
9d7512e9ab Update README 2020-05-06 08:08:43 -04:00
Deluan Quintão
2e31b4d046 Update translations (#264)
* Update it.json (POEditor.com)

* Update de.json (POEditor.com)

* Update pt.json (POEditor.com)

* Add Czech translation

* Update fr.json (POEditor.com)

* Update nl.json (POEditor.com)

* Update cs.json (POEditor.com)

* Update English translation

* Update it.json (POEditor.com)
2020-05-05 18:32:22 -04:00
Deluan
c585ca7131 Add random as a valid sort option for song resource 2020-05-05 16:17:09 -04:00
Deluan
29e2ab1b4a Set default view to Album list 2020-05-05 15:44:05 -04:00
Deluan
8880294ee7 Change default album view mode to Grid 2020-05-05 15:34:31 -04:00
Deluan
a8d3466b0e Unselect album songs after clicking on bulk "Play Later" button 2020-05-05 15:08:30 -04:00
Deluan
0ee000a8a0 Resolve TODO (workaround is necessary) 2020-05-05 12:35:50 -04:00
Deluan
0833d87f94 Add "Play Later" action to AlbumContextMenu 2020-05-05 12:20:41 -04:00
Deluan
23836d7c3c Change addTrack action to addTracks, supporting multiple tracks to be added to the queue in one call 2020-05-05 12:07:50 -04:00
Deluan
5495451448 Use only one call to the server when adding songs to the queue
Also show a message when there's an error communication with the server
2020-05-05 11:19:41 -04:00
Deluan
bb01c8973f Fix lint error 2020-05-04 20:46:16 -04:00
Deluan
2f4d4c6e38 Add missing translation terms 2020-05-04 20:27:09 -04:00
Deluan
8d99c3ab92 Add validation tests to translations files 2020-05-04 19:54:10 -04:00
Deluan Quintão
8f66e87099 Install reflex in setup-dev target 2020-05-04 17:07:01 -04:00
Deluan
3e778e6007 Bump github.com/sirupsen/logrus from 1.5.0 to 1.6.0 2020-05-04 13:28:16 -04:00
Deluan
b2d6dd0254 Add pr-# tag to Docker image 2020-05-04 12:19:20 -04:00
Deluan
589c4cf225 Fix screenshot proportion 2020-05-03 19:25:39 -04:00
Deluan
4b70cc52d6 Reduce log level of config file being used 2020-05-03 14:09:31 -04:00
Deluan
cc1205c79d Simplify README.md 2020-05-03 00:04:33 -04:00
Deluan
cccd0235cf Add option to specify ConfigFile path 2020-05-02 23:17:38 -04:00
Deluan
17e51756ef Removed dependencies for ra language files 2020-05-02 22:30:55 -04:00
Deluan
13ce21843f go mod tidy 2020-05-02 18:00:18 -04:00
Deluan
151f43b95f Refactor i18n functions a bit 2020-05-02 17:44:24 -04:00
Deluan
055c77b38c Remove "default" from Dark theme name 2020-05-02 14:50:46 -04:00
Deluan
8dc2d7a5e0 Make context menu icon smaller 2020-05-02 14:50:15 -04:00
Deluan
a71d5b3954 Add remaining languages 2020-05-02 14:19:01 -04:00
Deluan
854a923fea Don't sort ReadAll translations, as it will be sorted in the UI 2020-05-02 14:19:01 -04:00
Deluan
496b467c1d Cater for differences when loading embedded Assets and in dev mode 2020-05-02 14:19:01 -04:00
Deluan
056d5e7111 Remove empty keys to allow English fallback 2020-05-02 14:19:01 -04:00
Deluan Quintão
e43c172d96 Update de.json (POEditor.com) 2020-05-02 14:19:01 -04:00
Deluan Quintão
0b56c3f026 Update pt.json (POEditor.com) 2020-05-02 14:19:01 -04:00
Deluan Quintão
5445d20ecd Update en.json (POEditor.com) 2020-05-02 14:19:01 -04:00
Deluan
2f7443e4bd Use English as fallback language 2020-05-02 14:19:01 -04:00
Deluan
41cf99541d Move translations to server 2020-05-02 14:19:01 -04:00
Deluan
1a9663d432 Move static to resources. Embed them at build time 2020-05-02 14:19:01 -04:00
Deluan
b7dcdedf41 More error handling 2020-05-02 14:19:01 -04:00
Deluan
bf8f9d2be8 Fix context menu icon color on Light theme 2020-05-01 12:08:32 -04:00
Deluan
6d20ca27f6 Add mobile album list view 2020-05-01 11:50:07 -04:00
Deluan
3bb573b45f Add AlbumContextMenu to AlbumListView 2020-05-01 11:27:09 -04:00
Deluan
9b2d91c0f2 Fix songs pagination param in AlbumContextMenu 2020-05-01 11:05:36 -04:00
Deluan
b002a69bf8 Fix language sorting 2020-05-01 10:48:28 -04:00
Deluan
e341df1e26 Rename Chinese translation file to zh 2020-05-01 10:43:49 -04:00
Deluan
35e8c1c407 Add all translation keys to English 2020-05-01 10:41:47 -04:00
Deluan
d1a88ed8d6 Remove duplicated translation key 2020-05-01 10:28:31 -04:00
Deluan
10a7dfeb15 Add SongContextMenu 2020-05-01 10:22:24 -04:00
Deluan
dbde5330bd Mark helper function as unexported 2020-05-01 09:17:21 -04:00
Deluan
9b817edd1a go mod tidy 2020-05-01 09:08:35 -04:00
dependabot-preview[bot]
261d73410a Bump github.com/Masterminds/squirrel from 1.2.0 to 1.3.0
Bumps [github.com/Masterminds/squirrel](https://github.com/Masterminds/squirrel) from 1.2.0 to 1.3.0.
- [Release notes](https://github.com/Masterminds/squirrel/releases)
- [Commits](https://github.com/Masterminds/squirrel/compare/v1.2.0...v1.3.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-01 09:01:49 -04:00
Deluan
555c78f536 Reduce flickering of album covers 2020-05-01 09:00:00 -04:00
Deluan
0270a9c924 Remove dangling create-react-app README 2020-04-30 15:19:55 -04:00
Deluan
a45e278cda Bum react-music-player version to 4.12.0 2020-04-30 14:18:05 -04:00
stncrn
bdbee7f541 Add setup step: download node dependencies 2020-04-30 09:54:15 -04:00
Deluan
b453ee6598 Fix color of album context menu when in Light mode.
Fix is to make it always white
2020-04-29 22:46:34 -04:00
Deluan
716de24f1e Localize translation config notice 2020-04-29 21:59:05 -04:00
Deluan
c816ca4525 Add config option to enable/disable Transcoding configuration 2020-04-29 21:59:05 -04:00
Srihari Chandana
eb7d2dcaa1 fixed compile errors 2020-04-29 21:51:44 -04:00
Srihari Chandana
e6d4cfba96 cleaned up logic 2020-04-29 21:51:44 -04:00
Srihari Chandana
2a5d2d70ba replaced GridButton with GridMenu 2020-04-29 21:51:44 -04:00
Srihari Chandana
e539ddceb9 fixed code to remove warnings 2020-04-29 21:51:44 -04:00
Srihari Chandana
00666da9c1 added grid play button 2020-04-29 21:51:44 -04:00
Deluan
7ad9c385b5 Fix typo 2020-04-29 17:38:03 -04:00
Sumner Evans
e65fb189ce Added back configs that I totally missed because I was tired 2020-04-29 17:18:44 -04:00
Sumner Evans
1afe409a79 Update the sample navidrome.service for use in Arch Linux 2020-04-29 17:18:44 -04:00
jvoisin
dbf9c8be7d An other batch of linters 2020-04-29 14:09:45 -04:00
jvoisin
26188e6d8a Enable a couple of linters 2020-04-29 09:03:07 -04:00
Brian Pierson
d6c70554b3 Fixing 50 shades of blue 2020-04-29 08:15:28 -04:00
Deluan
5990a4285f Replace goreman with node-foreman 2020-04-28 23:24:57 -04:00
Deluan
08e9ac63b1 Add cron workflow to remove old pipeline artifacts 2020-04-28 14:13:34 -04:00
Deluan
71a1f65be2 Bump @testing-library dependencies 2020-04-28 12:06:05 -04:00
Deluan
5862157a2c Move test file to fixtures folder 2020-04-28 11:59:47 -04:00
Deluan
d4f17f2b73 Fix username English translation (fix #231) 2020-04-27 23:23:03 -04:00
Deluan
ea1d534c29 Fix NavBar title translations 2020-04-27 23:22:17 -04:00
Deluan
069de0f9ea Add a try catch to display the record when DurationField fails 2020-04-27 22:46:40 -04:00
Deluan
e871c7daee Add links to documentation on how to contribute with themes and translations 2020-04-27 20:43:58 -04:00
dependabot-preview[bot]
320fe11a66 Bump prettier from 2.0.4 to 2.0.5 in /ui
Bumps [prettier](https://github.com/prettier/prettier) from 2.0.4 to 2.0.5.
- [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.0.4...2.0.5)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-04-27 17:41:49 -04:00
Deluan
5fdc09a5b9 Fix pipeline (disable docker job when running on a PR from a forked repo) 2020-04-27 14:59:12 -04:00
Deluan
46f1b33812 Fix logging when first arg is a context.Context without a logger 2020-04-26 19:33:57 -04:00
Deluan
b44218fdcc Move the shuffleAlbum logic into an action 2020-04-26 19:15:52 -04:00
Deluan
4441ae1f0b Break up setup target, to avoid installing tools not required for building only 2020-04-26 16:10:40 -04:00
Deluan
1c3ee89ab4 Disable docker steps if secrets are not available 2020-04-26 15:52:21 -04:00
Deluan
ebc7964157 Fix formatting 2020-04-26 15:07:36 -04:00
Deluan
ad6c86d78a Check formatting in pipeline 2020-04-26 15:07:36 -04:00
Deluan
f3097496c6 Add golangci-lint to Go build step 2020-04-26 15:07:36 -04:00
Deluan
ddeefad501 Fix goimport and gosec warnings 2020-04-26 15:07:36 -04:00
Deluan
5cd453afeb Fix all errcheck warnings 2020-04-26 15:07:36 -04:00
Deluan
03c3c192ed Fixing static checks about passing nil context 2020-04-26 15:07:36 -04:00
Deluan
95790b9eff Remove unused code 2020-04-26 15:07:36 -04:00
ElleshaHackett
6bf7c751a1 Add Dutch language 2020-04-26 15:07:14 -04:00
Kevin Morssink
1019bb8258 Add Dutch language 2020-04-26 15:07:14 -04:00
Deluan
531155d016 Check if persistedState exists beforetrying to use it (fix #214) 2020-04-25 13:37:02 -04:00
Deluan
47311d16cf Trigger pipeline on new tags 2020-04-25 12:35:36 -04:00
Deluan
ef3466787d Fix the pipeline 2020-04-25 12:12:48 -04:00
Deluan
b7fd116bd8 Only triggers the pipeline on pushes to master and PRs 2020-04-25 12:06:05 -04:00
Deluan
34ad740e07 Enable French translation 2020-04-25 11:59:37 -04:00
Deluan
79454d7a92 Fix artist link contrast in light theme 2020-04-25 11:57:52 -04:00
Deluan
87cc397bc3 Add current playing track id to the Redux store 2020-04-25 11:57:52 -04:00
jvoisin
37602a2049 Bump the french traduction 2020-04-25 11:57:22 -04:00
Deluan
56ea380bb3 Add link to artist's albums on the album cover 2020-04-25 09:47:56 -04:00
Deluan
177ace1cee Turn off autoplay when reloading the play queue from the Redux store 2020-04-25 09:30:43 -04:00
Deluan
61e3fe21ff Add 'SNAPSHOT' to version when building locally, as this is not an "official" build 2020-04-25 09:27:22 -04:00
Deluan
8dcca76ec9 Fix various small sort issues 2020-04-24 17:37:28 -04:00
Deluan
1dd3a794f8 Reduce level of "invalid year" log message 2020-04-24 16:00:14 -04:00
Deluan
6c5dd245fe Parse TSO2 (seems that ffmpeg does not process this tag in some situations) 2020-04-24 15:02:20 -04:00
Deluan
3b3ad65612 Use order fields to sort by artist and album 2020-04-24 15:02:20 -04:00
Deluan
e6f798811d Generate Artist Index using the OrderArtistName 2020-04-24 15:02:20 -04:00
Deluan
371e8ab6ca Generate Order Fields based on sanitized version of original fields 2020-04-24 15:02:20 -04:00
Deluan
69c19e946c Add sort tags and use them in search 2020-04-24 15:02:20 -04:00
Deluan
d7edbf93f0 Make test more reliable
In some systems, it was detecting the `go.mod` file as an audio file, probably because of the system's mime-type configuration
2020-04-24 11:05:17 -04:00
Deluan
fb4d920fba Small change to trigger the pipeline 2020-04-23 22:29:33 -04:00
Deluan
5a072fbd10 Follow symlinks to directories when scanning 2020-04-23 20:31:44 -04:00
Deluan
79c9d8f4f4 Parameterize docker image name 2020-04-23 19:31:24 -04:00
Deluan
871bf5a70a Rename pipeline 2020-04-23 19:31:24 -04:00
Deluan
e4af235ce9 Move chmod to copy image, make the final image smaller 2020-04-23 19:31:24 -04:00
Deluan
00384a60f3 Unify GH actions 2020-04-23 19:31:24 -04:00
Deluan
f7b3ff4b34 Build and release docker images 2020-04-23 19:31:24 -04:00
Deluan
eaa48306fc Make Dockerfile platform independent
Thanks @0xERROR: https://github.com/deluan/navidrome/issues/92#issuecomment-614630429
2020-04-23 19:31:24 -04:00
Deluan
f5572b8447 Fix git tag detection 2020-04-23 19:31:24 -04:00
Deluan
a756751cc6 Build binary artifacts 2020-04-23 19:31:24 -04:00
Deluan
b8a3af090d Add cache to build workflow 2020-04-23 19:31:24 -04:00
Deluan
d534cb96a9 Replace math.Max with utils.MaxInt 2020-04-21 08:41:04 -04:00
Dimitri Herzog
f1e1d3bc07 request throttling only for media group api 2020-04-21 08:39:14 -04:00
Deluan
694be54428 Replace math.Max with utils.MaxInt 2020-04-20 12:17:01 -04:00
Deluan
76531fb1cd Remove old pre-commit script (in favour of lefthook) 2020-04-20 11:57:38 -04:00
Dimitri Herzog
716f4c5cf7 configuration for request throttling 2020-04-20 11:51:00 -04:00
jvoisin
ba2d4b6859 Add a .git-blame-ignore-revs file 2020-04-20 10:41:41 -04:00
Deluan
2ec5e47328 Set version correctly when building locally 2020-04-20 09:47:44 -04:00
Deluan
b3f70538a9 Upgrade Prettier to 2.0.4. Reformatted all JS files 2020-04-20 09:09:29 -04:00
Deluan
de115ff466 Bump Testing Library and moved it to devDependencies 2020-04-20 09:02:08 -04:00
Deluan
129f02b36b Bump ReactAdmin to 3.4.2 2020-04-20 08:50:21 -04:00
Deluan
1a8d219197 Remove generated comments from migrations 2020-04-19 23:29:08 -04:00
Deluan
80c8d85cb9 Fine tune search functionality 2020-04-19 23:29:07 -04:00
Deluan
db02f5f07f go mod tidy 2020-04-19 14:51:16 -04:00
Deluan
579294b0f1 Make Players and Transcodings view mobile-friendly 2020-04-19 13:54:51 -04:00
Deluan
f83d0d471d Fix getRandomSongs filters 2020-04-19 13:37:25 -04:00
Deluan Quintão
3b7d7bdb04 Disable French translation 2020-04-18 14:24:27 -04:00
jvoisin
05958f5195 Add French localization 2020-04-18 14:24:27 -04:00
Deluan
6cf4b81de9 Fix year range when querying by year 2020-04-18 14:05:44 -04:00
Deluan
689449df9e Force reindex to fix album by year searches 2020-04-18 11:08:54 -04:00
Deluan
dae938de6f Don't try to install Jamstash as part of initial setup 2020-04-17 22:11:58 -04:00
Deluan
f6617ff77d Add Chinese Simplified translation 2020-04-17 21:54:41 -04:00
Deluan
defdc2ea6b Bump Subsonic API to 1.10.2 2020-04-17 21:44:34 -04:00
Deluan
1fd6571a87 Refactored getSongsByGenre 2020-04-17 21:44:34 -04:00
Deluan
4c0250f9f8 Add fromYear/toYear params to getRandomSongs 2020-04-17 21:44:34 -04:00
Deluan
0e1735e7a9 Add getSongsByGenre endpoint 2020-04-17 21:44:34 -04:00
Deluan
a698e434fd Refactor list_generator to use new filters 2020-04-17 21:44:34 -04:00
Deluan
95f658336c Implement byYear and byGenre AlbumLists 2020-04-17 21:44:34 -04:00
Deluan
69dc4d97b3 Always fill album's min_year if max_year is filled 2020-04-17 21:44:34 -04:00
jvoisin
4aeb63c16e Add a couple of patterns to .gitignore 2020-04-17 10:06:35 -04:00
dependabot-preview[bot]
e5efadf99e Bump github.com/go-chi/chi from 4.1.0+incompatible to 4.1.1+incompatible
Bumps [github.com/go-chi/chi](https://github.com/go-chi/chi) from 4.1.0+incompatible to 4.1.1+incompatible.
- [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/v4.1.0...v4.1.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-04-17 08:09:06 -04:00
AlphaJack
d117d5794d Add Italian localization 2020-04-17 08:05:30 -04:00
Deluan
d09a2182e0 Lax Node version (only matches major version 13) 2020-04-17 00:21:42 -04:00
Deluan
b8b09820b1 Use deluan/ci-goreleaser 2020-04-16 17:44:12 -04:00
Deluan
2cfd7babb3 Add more Portuguese translations 2020-04-16 13:02:39 -04:00
Deluan
161a9b340c Add more Portuguese translations 2020-04-16 12:53:46 -04:00
Deluan
605253446a Fix AlbumLink label in Songs view 2020-04-16 10:26:24 -04:00
Deluan
f8d9b1508e Add prettier npm script 2020-04-15 22:11:23 -04:00
Deluan
3c4de3c8b5 Move language merge logic to i18n/index
This simplifies implementations one new languages
2020-04-15 22:11:23 -04:00
Deluan
a6c9bf1b15 Persist language selection to localStorage 2020-04-15 22:11:23 -04:00
Deluan
bf6ec67528 Add Language Selector to Personal settings 2020-04-15 22:11:23 -04:00
Deluan
289ba68824 Add Portuguese translation (incomplete) 2020-04-15 22:11:23 -04:00
Deluan
2dfe01963a Build binary for Linux MUSL (ex: Alpine). Fix #142 2020-04-15 08:49:30 -04:00
Deluan
5ed1d5c19f Upgrade github.com/djherbis/fscache to v0.10.1, tentatively fix #177 2020-04-15 08:45:10 -04:00
Deluan
db4479e720 Allow cache image to be disabled (workaround for #177) 2020-04-14 19:28:54 -04:00
Deluan
66275d3b94 Make song details table dense 2020-04-14 17:09:47 -04:00
Deluan
57f2c3f823 Better layout for Song Details 2020-04-14 16:21:59 -04:00
Deluan
afba4c9915 Add size and play count/date to Song Details 2020-04-14 15:23:11 -04:00
Deluan
f0d18d2cb3 Add Song Details to Album view 2020-04-14 14:59:16 -04:00
Deluan
da45bcf448 Make player theme configurable from Navidrome's theme 2020-04-14 11:54:49 -04:00
Deluan
3a54246b15 Change default sort for albums view to alphabetically (list) or most recent (grid) 2020-04-14 09:26:59 -04:00
Deluan
2b06f20f41 Close the sidebar menu when clicking "Personal" in mobile screens 2020-04-14 08:52:26 -04:00
Deluan
88f44b2e77 Upgrade React Player to 4.11.2, fix to MediaSession "close" action 2020-04-14 01:42:07 -04:00
Deluan
4dff067e0b Upgrade React Player to 4.11.1, enabled MediaSession 2020-04-13 14:24:50 -04:00
Deluan
d81bf8a518 Update github.com/go-chi/cors 2020-04-13 10:50:18 -04:00
Deluan
adfaf39489 Mark more endpoints as "gone" (won't be implemented) 2020-04-12 23:12:28 -04:00
Deluan
f6a15905d7 Move Album View toolbar to left 2020-04-12 20:43:51 -04:00
jvoisin
52b8c5f151 Correctly handle error in migration 2020-04-12 14:58:08 -04:00
Deluan
c4eab5db86 Update dhowden/tag library, to fix extracting images from Ogg files
see https://github.com/dhowden/tag/issues/64
2020-04-11 23:40:35 -04:00
Deluan
4b1c76e307 Keep the order of the playlist when adding new songs. Also allow adding a song more than once 2020-04-11 21:24:15 -04:00
Deluan
e476a5f6f1 Make fields songCount, duration, created and changed mandatory in playlists responses (fixes #164) 2020-04-11 19:15:15 -04:00
Deluan
9fb4f5ef52 Removed Playlist.GetWithTracks, not needed anymore 2020-04-11 19:05:51 -04:00
Deluan
e232c5c561 Add created and changed fields to playlists responses 2020-04-11 18:58:43 -04:00
Deluan
803a5776ae Update link to Subsonic API compatibility doc 2020-04-11 13:19:58 -04:00
Deluan
a6dfcafdab Update themes doc, link to documentation site 2020-04-11 13:13:53 -04:00
Deluan
8f2c7b7913 go mod tidy 2020-04-11 13:10:54 -04:00
jvoisin
2ab647efe1 Add a test 2020-04-11 13:08:21 -04:00
jvoisin
04eb421186 Refactor a bit how ffmpeg is used to get metadata
- createProbeCommand returns a []string instead of (string, string[])
- Simplify the loop of createProbeCommand
2020-04-11 13:08:21 -04:00
Deluan
6a3a66975c Update dhowden/tag library, to fix extracting images from some id3v4 tags
See https://github.com/dhowden/tag/issues/62
2020-04-10 23:42:06 -04:00
jvoisin
1ef4fa970f Simplify a bit ffmpeg's transcoder
- Remove the useless "format" parameter
- createTranscodeCommand now returns a list of string, instead of (string, string[])
2020-04-10 13:00:29 -04:00
jvoisin
b34523e196 Warn if ffmpeg can't be found 2020-04-10 10:56:58 -04:00
Deluan
09985453aa Show a Datagrid placeholder while loading 2020-04-09 22:38:40 -04:00
jvoisin
159a6e1cad Simplify the openrc unit 2020-04-09 19:21:23 -04:00
Deluan
b429949dd9 Keep optimistic rendering when changing the sort order for the current album 2020-04-09 18:53:44 -04:00
Deluan
b9f601dfb4 Remove unused import 2020-04-09 18:31:37 -04:00
Deluan
5b488b72b1 Add a custom AlbumSongs list component, to disable the optimistic rendering (should fix #158) 2020-04-09 18:28:47 -04:00
Deluan
03044bcb68 Ignore data folder when watching for changes in folders (when in dev mode) 2020-04-09 16:48:04 -04:00
Deluan
7bc3dace4c Revert "Improve ffmpeg's error diagnostic"
This reverts commit 4fc88f23
2020-04-09 14:26:42 -04:00
Deluan
c2ec142ce3 More tests 2020-04-09 13:36:05 -04:00
Deluan
2d39a6df8d Remove duplicated fscache creation 2020-04-09 13:15:01 -04:00
Deluan
5265d0234f Fix tests for Cover service 2020-04-09 12:13:54 -04:00
jvoisin
4fc88f23e9 Improve ffmpeg's error diagnostic
This should close #155
2020-04-09 10:40:16 -04:00
Deluan
5412bb2dc8 Fine tune album grid for mobile view 2020-04-09 09:53:53 -04:00
Deluan
b661d52477 Force full scan to enable search by tracks' artists in albums 2020-04-09 00:24:26 -04:00
Deluan
43ce81af67 Add all individual artists from album in searchable full text field. Should fix #94 2020-04-08 23:54:54 -04:00
Deluan
b8d1185f7f Remove duplicated words and extra spaces from full text searchable fields 2020-04-08 23:29:28 -04:00
Deluan
0fa8290ed3 Don't transcode if original format/bitrate is the same as the selected ones 2020-04-08 19:10:55 -04:00
Deluan
519e3f014d Re-stage files after formatting 2020-04-08 13:23:39 -04:00
Deluan
d38f8544d5 Remove unused localStorage config 2020-04-08 13:20:02 -04:00
Deluan
089a92157f Pass version to UI through AppConfig, instead of login payload.master
This makes the version info updated with a browser refresh (no need to logout and login again)
2020-04-08 11:00:30 -04:00
Deluan
db246900a6 Introduce a new configuration to select the login background image URL 2020-04-08 09:07:15 -04:00
Deluan
a0f389fc3e Consolidate UI configuration in one place, allowing it to be overridden from the server 2020-04-08 09:07:15 -04:00
Deluan
d0188db4f9 Fine tune album grid 2020-04-07 21:25:06 -04:00
Deluan
f537984bbf Use trackId instead of simply id, as it seems to conflict with internal id generated by the player. fixes #153 2020-04-07 11:55:45 -04:00
Deluan
7e6c0e3894 Less noisy logs for scrobble 2020-04-06 19:42:35 -04:00
Deluan
b930c7253a Fix tests in pipeline 2020-04-06 17:01:48 -04:00
Deluan
c1afe70d98 Fix: also pass the custom authorization header in all requests 2020-04-06 16:23:47 -04:00
Deluan
3f9ddb915e Use a custom authorization header, to avoid conflicts with proxies using basic auth (fixes #146) 2020-04-06 16:03:20 -04:00
Deluan
c3edc7f449 Add test for ServeIndex 2020-04-06 15:37:15 -04:00
Deluan
9b272c8021 Small log tweak 2020-04-06 14:02:50 -04:00
Deluan
6d1221164b Download and install latest Jamstash when calling make Jamstash-master 2020-04-06 00:40:51 -04:00
Deluan
647132625c Logs new stream sessions 2020-04-06 00:26:51 -04:00
Deluan
a17a98a75f Log API requests and responses at Debug level 2020-04-05 23:57:04 -04:00
Deluan
59707b3a8f Detect embedded art in ogg containers 2020-04-05 23:41:10 -04:00
Deluan
fa378ab4e4 Add tracing log to Cover service 2020-04-05 22:48:07 -04:00
Deluan
05ffb1acad Cache cover arts. closes #19 2020-04-05 22:02:06 -04:00
Deluan
a1ba5c59b2 Returns default cover on any error (not found, encoding, or unknown)
Only returns error if it cannot read the default image
2020-04-05 22:02:06 -04:00
Deluan
1bc68c20fc Create and configure image cache 2020-04-05 22:02:06 -04:00
Deluan
d308e7ca46 Fix typo 2020-04-05 17:49:14 -04:00
jvoisin
2b5433dc6e Add an openrc unit file 2020-04-05 13:07:00 -04:00
Deluan
86a23f9b14 Add more indexes to MediaFile table 2020-04-04 21:56:22 -04:00
Deluan
0ba5840a65 Don't set a playerId cookie it cannot register the player 2020-04-04 20:26:36 -04:00
Deluan
93646b964e More logging tests 2020-04-04 19:11:21 -04:00
Deluan
13be8d297c Converted last GoConvey tests to Ginkgo
Removed GoConvey dependency
2020-04-04 18:54:12 -04:00
Deluan
a4b97121ab Changes when pipelines are triggered:
- Build now on new Pull Requests
- Release only on new pushed tags
2020-04-04 16:39:43 -04:00
Deluan
660f9c205b Rename dist target to snapshot 2020-04-04 14:36:23 -04:00
Deluan
28852ce7d7 go mod tidy 2020-04-03 22:57:59 -04:00
Deluan
656ca1f3b5 Fix colour of album actions 2020-04-03 22:35:55 -04:00
Deluan
b8f7715a74 Fix ReactAdmin console warnings 2020-04-03 21:03:34 -04:00
Deluan
096ed396c8 Add link to all artist's albums from an album 2020-04-03 20:51:15 -04:00
Deluan
3b6d0b3d15 Add a catchall route to redirect everything to app/index.html 2020-04-03 19:45:35 -04:00
Deluan
75cd21da1f Add BaseURL configuration (fixes #103) 2020-04-03 19:05:38 -04:00
Deluan
b8eb22d162 Add git hooks on check_env 2020-04-03 16:00:17 -04:00
Deluan
9b461735f4 Add comments to createXxxxCommand functions to clarify about the filepaths arguments being absolute paths 2020-04-03 14:49:35 -04:00
Deluan
63bf49b3c4 Add lefthook for handling git hooks 2020-04-03 14:48:14 -04:00
Deluan
559848299c Fix default mp3 encoding ffmpeg command 2020-04-03 00:26:41 -04:00
Deluan
8510273216 Send estimated content length if requested 2020-04-03 00:24:40 -04:00
Deluan
2392060bc1 Don't try to transcode a file if the requested format is the same and the client is not requesting to downsample 2020-04-02 22:17:52 -04:00
Deluan
2d7998de59 Return cover from album even if client does not prefix the id with al-. Fixes #46 2020-04-02 22:03:27 -04:00
Deluan
40638688b2 Remove React warnings by omit properties not used downstream 2020-04-02 19:58:34 -04:00
Deluan
ea22b2fc6d Bump react-admin to 3.3.3 2020-04-02 19:47:10 -04:00
Deluan
1182218787 Upgrade Node to 13.12 2020-04-02 19:41:10 -04:00
Deluan
14f7c5610e Bump @testing-library/jest-dom, @testing-library/user-event and react-dom 2020-04-02 19:31:49 -04:00
Deluan
27579b99a3 Removed album list selection, for now 2020-04-02 19:20:39 -04:00
Deluan
c58021e645 Make Personal settings form more consistent with the rest of the App 2020-04-02 18:46:09 -04:00
Deluan
1810cc7ac7 Simplify album lists tabs handling 2020-04-02 18:18:52 -04:00
Deluan
86f73eecca Only add padding to layout if the player is visible 2020-04-02 18:09:02 -04:00
Deluan
3d6ce8a77f Skip calling ffmpeg if there are no files to probe 2020-04-02 17:38:20 -04:00
Deluan
670be29d7b Revert "Pause the player with <space>"
This reverts commit 6e6cfdd0
2020-04-02 16:52:46 -04:00
dependabot-preview[bot]
2b3e506583 build(deps): bump github.com/go-chi/chi
Bumps [github.com/go-chi/chi](https://github.com/go-chi/chi) from 4.0.4+incompatible to 4.1.0+incompatible.
- [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/v4.0.4...v4.1.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-04-02 08:53:28 -04:00
jvoisin
6e6cfdd02b Pause the player with <space> 2020-04-01 17:14:09 -04:00
Deeparghya Dutta Barua
a18093e255 Fix systemd unit to allow FFmpeg execution 2020-04-01 15:15:59 -04:00
Deluan
a35636999d feat: fine tune album art image size. better, but still not ideal 2020-04-01 09:09:51 -04:00
Deluan
13a3d38e4f fix: Personal view title and menu tooltip 2020-03-31 21:40:06 -04:00
Deluan
9f00fb0f05 feat: move Configuration menu to Personal settings 2020-03-31 21:28:50 -04:00
Deluan
6cddcd6f0d docs: update README 2020-03-31 20:42:59 -04:00
Deluan Quintão
c6d1cfeceb docs: update theme's README 2020-03-31 20:07:11 -04:00
Deluan
de43c27b3c docs: basic documentation on creating themes.
#129
2020-03-31 19:54:38 -04:00
Deluan
747b5ea25e refactor: rename theme name attribute to themeName 2020-03-31 19:36:45 -04:00
Deluan
dd2e98fca2 feat: make theme select input longer 2020-03-31 18:43:54 -04:00
Deluan
eb621be646 feat: load themes dynamically 2020-03-31 18:31:14 -04:00
Deluan
d223a4f4db docs: mentions our Subreddit in the README 2020-03-31 15:11:33 -04:00
Deluan
7aa182e33d fix: add padding at the bottom of the layout, to accommodate the audio player (relates to #132) 2020-03-31 14:52:54 -04:00
Deluan
7fec503b72 feat: persist the queue in the localStorage 2020-03-31 14:34:08 -04:00
Deluan
083a11a563 feat: store state in localStorage 2020-03-31 14:07:33 -04:00
Deluan
944f3695c4 fix: disable click on version menu item 2020-03-31 13:04:04 -04:00
Deluan
dfc8691262 fix: add "Version" message to translations 2020-03-31 11:17:11 -04:00
Deluan
395b598bb1 fix: don't show tooltips in profile menu items 2020-03-31 11:07:45 -04:00
Deluan
d04b434d96 fix: profile menu items colors 2020-03-31 10:49:47 -04:00
Deluan
f041503a85 feat: simple theme selector. only works with hardcoded light and dark for now 2020-03-31 09:35:44 -04:00
Deluan
500207f7b8 refactor: extract themes to their own folder 2020-03-31 09:05:46 -04:00
Deluan
1e0a79ebb7 fix: "Recent" should sort by play_date, not starred_at 2020-03-30 19:34:44 -04:00
Deluan
301fa2a957 fix: sort by album in songs view 2020-03-30 19:34:00 -04:00
Deluan
46f4f63212 feat: initial implementation of album lists 2020-03-29 00:01:08 -04:00
Deluan
fec8b5f731 feat: add playcounts to album and songs
(fix year in song list)
2020-03-28 20:38:41 -04:00
Deluan
777231ea79 feat: expose album, song and artist annotations in the RESTful API 2020-03-28 19:22:55 -04:00
Deluan
0e36ed35a3 fix: typo 2020-03-28 18:50:18 -04:00
Deluan
f1af646cee feat: option to display albums as a grid 2020-03-28 16:25:55 -04:00
Deluan
fc0621646b feat: add link to album from Songs view 2020-03-28 00:34:09 -04:00
Deluan
575800dcff docs: add badge with link to subreddit 2020-03-27 21:51:24 -04:00
Deluan
0ca849a61a feat: show year range in album view and match ranges in year filter. #118 2020-03-27 21:11:06 -04:00
Deluan
53e8a92fed feat: rename year to max_year and add min_year to album. #118 2020-03-27 21:11:06 -04:00
Deluan
fc650cd127 chore: upgrade to Node 13.11 2020-03-27 19:23:52 -04:00
Deluan
b03519b09c fix: configured transcodings not appearing in players view 2020-03-27 19:12:11 -04:00
Deluan
39b9f818be feat: use ND_PORT env var in health check 2020-03-26 15:26:40 -04:00
Deluan
7febe05ed5 feat: add health check to docker image 2020-03-26 15:15:40 -04:00
Deluan
2c42e4e12e feat: add icons for playlists 2020-03-26 12:33:30 -04:00
Deluan
dcb3b3b5d1 fix: various album_artists <-> artists mismatches 2020-03-26 09:08:53 -04:00
Deluan
5331732236 fix: remove sql injection 2020-03-25 20:40:18 -04:00
Deluan
dc973ae670 refactor: remove unused code 2020-03-25 20:40:18 -04:00
Deluan
100db2bcfd feat: add artist filter to album view 2020-03-25 20:40:18 -04:00
dependabot-preview[bot]
c84a58ff7d build(deps): bump github.com/go-chi/chi
Bumps [github.com/go-chi/chi](https://github.com/go-chi/chi) from 4.0.3+incompatible to 4.0.4+incompatible.
- [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/v4.0.3...v4.0.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-03-25 18:12:33 -04:00
Deluan
2d7fda1b2f docs: add default config vars to docker-compose.yml example 2020-03-24 12:34:31 -04:00
Deluan
3cba5f70fd chore: add tests for all utils, removed unused functions 2020-03-24 11:59:10 -04:00
Deluan
b4c7cac964 refactor: moved magic strings to consts 2020-03-24 11:59:10 -04:00
dependabot-preview[bot]
5ef80d2490 build(deps): bump github.com/sirupsen/logrus from 1.4.2 to 1.5.0
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.4.2 to 1.5.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.4.2...v1.5.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-03-24 09:09:12 -04:00
dependabot-preview[bot]
3b798cf943 build(deps): bump react-scripts from 3.4.0 to 3.4.1 in /ui
Bumps [react-scripts](https://github.com/facebook/create-react-app/tree/HEAD/packages/react-scripts) from 3.4.0 to 3.4.1.
- [Release notes](https://github.com/facebook/create-react-app/releases)
- [Changelog](https://github.com/facebook/create-react-app/blob/master/CHANGELOG.md)
- [Commits](https://github.com/facebook/create-react-app/commits/react-scripts@3.4.1/packages/react-scripts)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-03-23 08:49:40 -04:00
dependabot-preview[bot]
50b7756159 build(deps): bump react from 16.13.0 to 16.13.1 in /ui
Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) from 16.13.0 to 16.13.1.
- [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.13.1/packages/react)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-03-23 08:48:48 -04:00
Deluan
15606770ca chore: removed non-working config flag 2020-03-22 01:13:55 -04:00
Deluan
f403a8da34 feat: add version to index.html description meta tag 2020-03-22 01:04:10 -04:00
Deluan
20075ae68d refactor: extracted restful helpers into their own composable struct 2020-03-21 20:00:46 -04:00
Deluan
91a743623a feat: always show artist name in Album view 2020-03-21 19:15:39 -04:00
Deluan
e23a290812 fix: logging of scanner startup 2020-03-21 14:20:22 -04:00
Deluan
dee68559ab docs: uses less space for client list 2020-03-21 14:11:57 -04:00
Deluan
9f42e330b4 fix: change web requests log level to debug 2020-03-21 13:03:04 -04:00
jvoisin
ad63b8b1b4 Add a systemd startup unit 2020-03-21 12:47:05 -04:00
Deluan
0d8a2b310f fix: the default session timeout must be 30 minutes, not seconds! 2020-03-21 12:17:20 -04:00
Deluan
3977575563 build: add a simple build as default target, trying to make LGTM work 2020-03-20 12:21:41 -04:00
Deluan
47244cb770 refactor: remove unused static file 2020-03-20 12:00:14 -04:00
Deluan
57aaf5a26b refactor: remove unused property 2020-03-20 00:30:16 -04:00
Deluan
352d686d94 chore: upgrade react-admin to 3.3.1 2020-03-20 00:23:04 -04:00
Deluan
f6e448c1ba refactor: removed unused code, unnecessary typecasts and fixed small warnings 2020-03-20 00:07:36 -04:00
Deluan
270b0ae74e feat: add "Compilation" filter to albums 2020-03-19 23:25:40 -04:00
Deluan
8401d85f78 feat: search in WebUI now is more flexible, searching in all relevant fields in the current view 2020-03-19 22:26:18 -04:00
Deluan
32fbf2e9eb refactor: drop search table, integrated full_text into main tables 2020-03-19 21:44:48 -04:00
Deluan
8cdd4e317d feat: allow restful filter customization per field 2020-03-19 21:09:57 -04:00
Deluan
97d95ea794 fix: group compilations together in the restful API. fix #93 2020-03-19 15:02:11 -04:00
Deluan
cbbebb3264 fix: version position under banner 2020-03-18 23:21:01 -04:00
Deluan
8b108905a3 feat: use Navidrome's icon in getAvatar 2020-03-18 22:46:47 -04:00
Deluan
5b40ec400e build: go mod tidy 2020-03-18 21:35:15 -04:00
Deluan
29e661e1fe docs: update README 2020-03-18 21:23:45 -04:00
Deluan
b466ec75a4 build: always add latest tag to version 2020-03-18 21:05:17 -04:00
Deluan
c8cd755451 feat: use human readable sizes in cache size configuration 2020-03-18 20:39:10 -04:00
Deluan
faac303eff feat: allow session timeout to be configurable. closes #101 2020-03-18 20:16:18 -04:00
Deluan
ced87be57b fix: when searching player by id, create new player if client name does not match the one found 2020-03-17 19:10:09 -04:00
Deluan
811703ab60 fix: create default transcodings on existing installations 2020-03-17 16:49:37 -04:00
Deluan
bc1f767123 docs: Update README 2020-03-17 15:22:37 -04:00
Deluan
7055dc514b docs: update basic transcoding info 2020-03-17 15:20:35 -04:00
Deluan
e02f3d3ec9 refactor: clean up unused config options 2020-03-17 15:20:35 -04:00
Deluan
68a49befc8 feat: allow regular users to change their players' configuration 2020-03-17 15:20:35 -04:00
Deluan
c8b0d2bfae feat: select correct transcoding for streaming 2020-03-17 15:20:35 -04:00
Deluan
39993810b3 feat: add transcodedSuffix to Subsonic API responses 2020-03-17 15:20:35 -04:00
Deluan
45180115a6 feat: player CRUD 2020-03-17 15:20:35 -04:00
Deluan
353c48d8d8 refactor: rename player to audioplayer 2020-03-17 15:20:35 -04:00
Deluan
da36941252 feat: better getPlayer middleware setup 2020-03-17 15:20:35 -04:00
Deluan
8ec78900c5 feat: transcoding and player datastores and configuration 2020-03-17 15:20:35 -04:00
dependabot-preview[bot]
a0e0fbad58 build(deps): bump @testing-library/react from 9.5.0 to 10.0.1 in /ui
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 9.5.0 to 10.0.1.
- [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/v9.5.0...v10.0.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-03-16 10:06:23 -04:00
dependabot-preview[bot]
75e7ba8b1e build(deps): bump github.com/go-chi/cors from 1.0.0 to 1.0.1
Bumps [github.com/go-chi/cors](https://github.com/go-chi/cors) from 1.0.0 to 1.0.1.
- [Release notes](https://github.com/go-chi/cors/releases)
- [Commits](https://github.com/go-chi/cors/compare/v1.0.0...v1.0.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-03-16 10:04:16 -04:00
Deluan
74c30b5a66 docs: add list of tested clients 2020-03-15 13:26:48 -04:00
Deluan Quintão
e67bdbbc32 docs: add link to transcoding issue 2020-03-15 13:09:43 -04:00
Deluan
9554c8f783 build: rename generated archives 2020-03-14 21:09:39 -04:00
Deluan
e36a42f356 build: generate binaries for Linux armv6, armv7 and arm68 (v8) (fixes #92) 2020-03-14 21:09:39 -04:00
dependabot-preview[bot]
9d1960232c build(deps): [security] bump acorn from 5.7.3 to 5.7.4 in /ui
Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.3 to 5.7.4. **This update includes a security fix.**
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.3...5.7.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-03-13 18:01:31 -04:00
Deluan
d3547544bf feat: new WebUI icon 2020-03-11 20:18:22 -04:00
Deluan
9cb42606ba fix: force full rescan to enable search by album artist 2020-03-10 17:23:25 -04:00
dependabot-preview[bot]
7772afce1c build(deps): bump @testing-library/react from 9.4.1 to 9.5.0 in /ui
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 9.4.1 to 9.5.0.
- [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/v9.4.1...v9.5.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-03-10 17:11:46 -04:00
Deluan
10e76257c6 fix: increase contrast in WebUI's dark theme 2020-03-08 16:12:12 -04:00
Deluan
9235ab6414 fix: index albumArtist as part of the album searchable fields 2020-03-07 13:10:20 -05:00
Deluan
59356f0029 refactor: removed indirect call introduced by intellij's refactor 2020-03-06 16:28:20 -05:00
Deluan
9ae14015a1 build: get Go and Node versions from go.mod and .nvmrc respectively 2020-03-06 11:50:39 -05:00
Deluan
0b131e91c1 chore: upgrade to NodeJS 13.10 2020-03-06 10:57:00 -05:00
Deluan
77b12eafde chore: upgrade react-jinke-music-player 2020-03-05 08:57:40 -05:00
Deluan
050778460d fix: missing id in queue items was preventing scrobble to work properly 2020-03-04 10:30:58 -05:00
Deluan
28bc9c1d4f fix: AlbumShow was adding previous played tracks when trying to shuffle the album 2020-03-02 14:51:52 -05:00
Deluan
5e7aaa667b fix: missing id in queue items was preventing scrobble to work properly 2020-03-02 14:20:57 -05:00
Deluan
1afc495920 chore: upgrade react, react-dom and react-redux 2020-03-02 13:06:27 -05:00
Deluan
cf7d877714 chore: upgrade @testing-library/user-event 2020-03-02 12:04:58 -05:00
Deluan
81831da67a chore: upgrade react-admin 2020-03-02 11:58:23 -05:00
Deluan
fcd2fcae67 chore: upgrade @testing-library, react-scripts 2020-03-02 11:52:06 -05:00
Deluan
1c33b0aea8 docs: update API compatibility chart 2020-03-02 09:48:46 -05:00
Deluan
fc06163b5a refactor: remove superfluous (and untested) code 2020-03-02 09:37:47 -05:00
Deluan
72f0a6fb66 chore: removed unused (video) mime types 2020-03-02 00:16:15 -05:00
Deluan
6f5a322927 fix: login must be case-insensitive 2020-03-01 15:45:41 -05:00
Deluan
a7f8e4ee2b fix: only set created_at when adding data to DB 2020-02-28 18:43:22 -05:00
Deluan
0850872b0f fix: ormer.Driver() is not available when creating orms with NewOrmWithDB() 2020-02-28 16:09:27 -05:00
Deluan
1d886156d5 feat: better SQLite3 configuration, to avoid DB contention 2020-02-28 15:06:31 -05:00
Deluan
faa2a978c0 refactor: use only one DB instance for the whole application 2020-02-28 15:06:31 -05:00
Deluan
38faffa907 feat: notice function to notify (in logs) about important changes in migrations 2020-02-28 14:00:41 -05:00
Deluan
65a792be3a fix: handle nil pointer dereference 2020-02-28 11:02:38 -05:00
Deluan
876354e58e feat: MaxTranscodingCacheSize is now specified in MB 2020-02-26 14:08:14 -05:00
Deluan
14b33bc34d fix: there are no docker images available for node 13.9 2020-02-26 12:00:00 -05:00
Deluan
9044aa8740 chore: upgrade NodeJS to 13.9.0 2020-02-26 09:52:25 -05:00
Deluan
07ac14f810 chore: upgrade Go to 1.14 2020-02-26 09:37:48 -05:00
Deluan
0370f0a3ea refactor: rename ffmpeg to transcoder 2020-02-25 10:32:34 -05:00
Deluan
33ede13eef fix: check if album is starred before adding the starred date in the response. also return "starred" in search responses 2020-02-24 22:06:12 -05:00
Deluan
e032bfcf6b refactor: make parameters consistent 2020-02-24 19:04:54 -05:00
Deluan
f4014c475d refactor: make fakeFFmpeg more configurable, change test name 2020-02-24 14:17:32 -05:00
Deluan
f394de664a refactor: new transcoding engine. third (fourth?) time is a charm! 2020-02-24 13:56:09 -05:00
Deluan
d2eea64528 fix: typo 2020-02-23 21:41:10 -05:00
Deluan
d7b5e6a36c fix: add public attribute to playlists. Even though it is optional,
DSub requires it
2020-02-23 00:10:05 -05:00
Deluan
b49b9e3ca0 chore: remove unused script 2020-02-22 20:29:57 -05:00
Deluan
1322bb3bf3 refactor: move cache constructor 2020-02-21 09:36:29 -05:00
Deluan
13a046a679 fix: change stream cache eviction check period to every 10 minutes 2020-02-20 20:12:52 -05:00
Deluan
e6d2056438 fix: typo 2020-02-20 19:39:32 -05:00
Deluan
a6b0c57ce0 feat: add a proper caching system to the transcoding functionality 2020-02-20 19:25:39 -05:00
Deluan
fc14e346b9 feat: store duration as float, to cater for milliseconds 2020-02-20 17:02:06 -05:00
Deluan
5525145906 fix: audio stream's bitrate has precedence over container's bitrate 2020-02-20 13:56:45 -05:00
Deluan
74d87790b8 refactor: better ffmpeg output metadata parsing 2020-02-20 10:41:16 -05:00
Deluan
8ce796756f fix: error message 2020-02-19 15:34:05 -05:00
Deluan
a412989f7e refactor: more stable transcoder, based on http.FileSystem 2020-02-19 14:53:35 -05:00
Deluan
ae02dc203e chore: remove unused code 2020-02-19 09:08:05 -05:00
Deluan
fc7595a464 fix: cover art detection regex 2020-02-18 11:19:22 -05:00
Deluan
4ceaea7732 fix: extract stream level metadata 2020-02-18 10:00:05 -05:00
397 changed files with 18948 additions and 5686 deletions

View File

@@ -1,8 +1,8 @@
.DS_Store
ui/node_modules
ui/build
Jamstash-master
Dockerfile
docker-compose*.yml
data
*.db
testDB
@@ -10,4 +10,4 @@ navidrome
navidrome.db
navidrome.toml
assets/*gen.go
dist

2
.git-blame-ignore-revs Normal file
View File

@@ -0,0 +1,2 @@
# Upgrade Prettier to 2.0.4. Reformatted all JS files
b3f70538a9138bc279a451f4f358605097210d41

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
liberapay: deluan
ko_fi: 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']

View File

@@ -1,53 +0,0 @@
name: Build
on: [push]
jobs:
go:
name: Test Server on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
# TODO Fix tests in Windows
# os: [macOS-latest, ubuntu-latest, windows-latest]
os: [macOS-latest, ubuntu-latest]
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v1
with:
go-version: 1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Download dependencies
run: go mod download
- name: Test
run: go test -cover ./... -v
js:
name: Test UI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: 13
- name: npm install dependencies
run: |
cd ui
npm ci
# TODO: Enable when there are tests to run
# - name: npm test
# run: |
# cd ui
# CI=test npm test
- name: npm build
run: |
cd ui
npm run build

22
.github/workflows/docker-tags.sh vendored Executable file
View File

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

37
.github/workflows/pipeline.dockerfile vendored Normal file
View File

@@ -0,0 +1,37 @@
#####################################################
### Copy platform specific binary
FROM bash as copy-binary
ARG TARGETPLATFORM
RUN echo "Target Platform = ${TARGETPLATFORM}"
COPY dist .
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then cp navidrome_linux_amd64_linux_amd64/navidrome /navidrome; fi
RUN if [ "$TARGETPLATFORM" = "linux/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
RUN chmod +x /navidrome
#####################################################
### Build Final Image
FROM alpine as release
LABEL maintainer="deluan@navidrome.org"
# Install ffmpeg and output build config
RUN apk add --no-cache ffmpeg
RUN ffmpeg -buildconf
COPY --from=copy-binary /navidrome /app/
VOLUME ["/data", "/music"]
ENV ND_MUSICFOLDER /music
ENV ND_DATAFOLDER /data
ENV ND_PORT 4533
ENV GODEBUG "asyncpreemptoff=1"
EXPOSE ${ND_PORT}
HEALTHCHECK CMD wget -O- http://localhost:${ND_PORT}/ping || exit 1
WORKDIR /app
ENTRYPOINT ["/app/navidrome"]

169
.github/workflows/pipeline.yml vendored Normal file
View File

@@ -0,0 +1,169 @@
name: Pipeline
on:
push:
branches:
- master
tags:
- "v*"
pull_request:
branches:
- master
jobs:
golangci-lint:
name: Lint Server
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v1
with:
version: v1.27
github-token: ${{ secrets.GITHUB_TOKEN }}
go:
name: Test Server on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
# TODO Fix tests in Windows
# os: [macOS-latest, ubuntu-latest, windows-latest]
os: [macOS-latest, ubuntu-latest]
steps:
- name: Set up Go 1.14
uses: actions/setup-go@v1
with:
go-version: 1.14
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- uses: actions/cache@v1
id: cache-go
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Download dependencies
if: steps.cache-go.outputs.cache-hit != 'true'
run: go mod download
- name: Test
run: go test -cover ./... -v
js:
name: Build JS bundle
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 14
- uses: actions/cache@v1
id: cache-npm
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('ui/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: npm install dependencies
run: |
cd ui
npm ci
- name: npm check-formatting
run: |
cd ui
npm run check-formatting
- name: npm build
run: |
cd ui
npm run build
- uses: actions/upload-artifact@v2
with:
name: js-bundle
path: ui/build
binaries:
name: Binaries
needs: [js, go, golangci-lint]
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/download-artifact@v2
with:
name: js-bundle
path: ui/build
- name: Show Tags
run: git tag
- name: Show Version
run: git describe --tags
- name: Run GoReleaser - SNAPSHOT
if: startsWith(github.ref, 'refs/tags/') != true
uses: docker://deluan/ci-goreleaser:1.14.4-2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: goreleaser release --rm-dist --skip-publish --snapshot
- name: Run GoReleaser - RELEASE
if: startsWith(github.ref, 'refs/tags/')
uses: docker://deluan/ci-goreleaser:1.14.4-2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: goreleaser release --rm-dist
- uses: actions/upload-artifact@v2
with:
name: binaries
path: |
dist
!dist/*.tar.gz
!dist/*.zip
docker:
name: Docker images
needs: [binaries]
runs-on: ubuntu-latest
env:
DOCKER_IMAGE: ${{secrets.DOCKER_IMAGE}}
steps:
- 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
if: env.DOCKER_IMAGE != ''
- uses: actions/download-artifact@v2
if: env.DOCKER_IMAGE != ''
with:
name: binaries
path: dist
- name: Build the Docker image and push
if: env.DOCKER_IMAGE != ''
env:
DOCKER_IMAGE: ${{secrets.DOCKER_IMAGE}}
DOCKER_PLATFORM: linux/amd64,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,32 +0,0 @@
name: Release
on:
create:
tags:
- v*.*.*
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v1
with:
fetch-depth: 0
- uses: actions/setup-node@v1
with:
node-version: 13
- name: Build UI
run: |
cd ui
npm ci
npm run build
- name: Fetch tags
run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
- name: Run GoReleaser
uses: docker://bepsays/ci-goreleaser:latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: goreleaser release --rm-dist

View File

@@ -0,0 +1,18 @@
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

10
.gitignore vendored
View File

@@ -1,5 +1,6 @@
.DS_Store
.idea
.vscode
.envrc
/navidrome
/iTunes*.xml
@@ -9,14 +10,17 @@ vendor/*/
wiki
TODO.md
var
Artwork
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

29
.golangci.yml Normal file
View File

@@ -0,0 +1,29 @@
linters:
enable:
- bodyclose
- deadcode
- dogsled
- errcheck
- gocyclo
- goimports
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- interfacer
- misspell
- rowserrcheck
- staticcheck
- structcheck
- typecheck
- unconvert
- unused
- varcheck
- whitespace
issues:
exclude-rules:
- linters:
- gosec
text: "(G501|G401):"

View File

@@ -1,10 +1,10 @@
# GoReleaser config
project_name: navidrome
before:
hooks:
- go get -u github.com/go-bindata/go-bindata/...
- 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/...
- git checkout .
builds:
- id: navidrome_darwin
@@ -19,9 +19,9 @@ builds:
flags:
- -tags=embed
ldflags:
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Tag}}
- -s -w -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
- id: navidrome_linux
- id: navidrome_linux_amd64
env:
- CGO_ENABLED=1
goos:
@@ -32,7 +32,39 @@ builds:
- -tags=embed
ldflags:
- "-extldflags '-static'"
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Tag}}
- -s -w -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
- id: navidrome_linux_arm
env:
- CGO_ENABLED=1
- CC=arm-linux-gnueabi-gcc-5
goos:
- linux
goarch:
- arm
goarm:
- 5
- 6
- 7
flags:
- -tags=embed
ldflags:
- "-extldflags '-static'"
- -s -w -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
- id: navidrome_linux_arm64
env:
- CGO_ENABLED=1
- CC=aarch64-linux-gnu-gcc-5
goos:
- linux
goarch:
- arm64
flags:
- -tags=embed
ldflags:
- "-extldflags '-static'"
- -s -w -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
- id: navidrome_windows_i686
env:
@@ -47,7 +79,7 @@ builds:
- -tags=embed
ldflags:
- "-extldflags '-static'"
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Tag}}
- -s -w -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
- id: navidrome_windows_x64
env:
@@ -62,24 +94,30 @@ builds:
- -tags=embed
ldflags:
- "-extldflags '-static'"
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Tag}}
- -s -w -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
archives:
-
format_overrides:
- format_overrides:
- goos: windows
format: zip
replacements:
darwin: macOS
linux: Linux
windows: Windows
386: i386
amd64: x86_64
checksum:
name_template: 'checksums.txt'
name_template: "{{ .ProjectName }}_checksums.txt"
snapshot:
name_template: "{{ .Tag }}-next"
name_template: "{{ .Tag }}-SNAPSHOT"
release:
draft: true
changelog:
sort: asc
# sort: asc
filters:
exclude:
- '^docs:'
- "^docs:"

2
.nvmrc
View File

@@ -1 +1 @@
v13.7.0
v14

View File

@@ -1,69 +0,0 @@
### Supported Subsonic API endpoints
Navidrome is currently compatible with [Subsonic API](http://www.subsonic.org/pages/api.jsp) v1.8.0, with some exceptions.
This is an (almost) up to date list of all Subsonic API endpoints implemented by Navidrome.
Check the "Notes" column for limitations/missing behaviour. Also keep in mind these differences between
Navidrome and Subsonic:
* Right now, Navidrome only works with a single Music Library (Music Folder)
* Navidrome does not mark songs as played by calls to `stream`, only when
`scrobble` is called with `submission=true`
* Next features to be implemented: Playlists (WIP), MultiUser (WIP), Jukebox, Sharing, Podcasts, Bookmarks, Internet Radio.
Navidrome is actively being tested with:
[DSub](http://www.subsonic.org/pages/apps.jsp#dsub),
[Music Stash](https://play.google.com/store/apps/details?id=com.ghenry22.mymusicstash) and
[Jamstash](http://www.subsonic.org/pages/apps.jsp#jamstash))
| ENDPOINT | NOTES |
|------------------------|-------|
| _SYSTEM_ ||
| `ping` | |
| `getLicense` | Always valid ;) |
| ||
| _BROWSING_ ||
| `getMusicFolders` | Hardcoded to just one, set with ND_MUSICFOLDER configuration |
| `getIndexes` | Doesn't support shortcuts, nor direct children |
| `getMusicDirectory` | |
| `getSong` | |
| `getArtists` | |
| `getArtist` | |
| `getAlbum` | |
| `getGenres` | |
| ||
| _ALBUM/SONGS LISTS_ ||
| `getAlbumList` | `byYear` and `byGenre` are not implemented |
| `getAlbumList2` | `byYear` and `byGenre` are not implemented |
| `getStarred` | |
| `getStarred2` | |
| `getNowPlaying` | |
| `getRandomSongs` | Ignores `fromYear` and `toYear` parameters |
| ||
| _SEARCHING_ ||
| `search2` | Doesn't support Lucene queries, only simple auto complete queries |
| `search3` | Doesn't support Lucene queries, only simple auto complete queries |
| ||
| _PLAYLISTS_ ||
| `getPlaylists` | `username` parameter is not implemented |
| `getPlaylist` | |
| `createPlaylist` | Return empty response on success |
| `updatePlaylist` | `comment` and `public` are not implemented. All playlists are public |
| `deletePlaylist` | |
| ||
| _MEDIA RETRIEVAL_ ||
| `stream` | No Transcoding/Downsampling support (for now)|
| `download` | |
| `getCoverArt` | Only gets embedded artwork |
| `getAvatar` | Always returns the same image |
| ||
| _MEDIA ANNOTATION_ ||
| `star` | |
| `unstar` | |
| `setRating` | Doesn't work with artists |
| `scrobble` | No Last.FM support yet. It is used to update play count and last played |
| ||
| _USER MANAGEMENT_ ||
| `getUser` | Hardcoded all roles, ignores `username` parameter|

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.

View File

@@ -1,73 +0,0 @@
#####################################################
### Build UI bundles
FROM node:13.7-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.13-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 and unpack static ffmpeg
ARG FFMPEG_URL=https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
RUN wget -O /tmp/ffmpeg.tar.xz ${FFMPEG_URL}
RUN cd /tmp && tar xJf ffmpeg.tar.xz && rm ffmpeg.tar.xz
# 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 name-rev --name-only HEAD) && \
GIT_TAG=${GIT_TAG#"tags/"} && \
GIT_SHA=$(git rev-parse --short HEAD) && \
echo "Building version: ${GIT_TAG} (${GIT_SHA})" && \
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
MAINTAINER Deluan Quintao <navidrome@deluan.com>
# Download Tini
ENV TINI_VERSION v0.18.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static /tini
RUN chmod +x /tini
COPY --from=gobuilder /src/navidrome /app/
COPY --from=gobuilder /tmp/ffmpeg*/ffmpeg /usr/bin/
# Check if ffmpeg runs properly
RUN ffmpeg -buildconf
VOLUME ["/data", "/music"]
ENV ND_MUSICFOLDER /music
ENV ND_DATAFOLDER /data
ENV ND_SCANINTERVAL 1m
ENV ND_LOGLEVEL info
ENV ND_PORT 4533
EXPOSE 4533
WORKDIR /app
ENTRYPOINT ["/tini", "--"]
CMD ["/app/navidrome"]

View File

@@ -1,74 +1,98 @@
GO_VERSION=1.13
NODE_VERSION=v13.7.0
GO_VERSION=$(shell grep -e "^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
.PHONY: dev
dev: check_env
@goreman -f Procfile.dev -b 4533 start
npx foreman -j Procfile.dev -p 4533 start
.PHONY: dev
.PHONY: server
server: check_go_env
@reflex -d none -c reflex.conf
.PHONY: server
wire: check_go_env
wire ./...
.PHONY: wire
.PHONY: watch
watch: check_go_env
ginkgo watch -notify ./...
.PHONY: watch
.PHONY: test
test: check_go_env
go test ./... -v
# @(cd ./ui && npm test -- --watchAll=false)
.PHONY: test
.PHONY: testall
testall: check_go_env test
@(cd ./ui && npm test -- --watchAll=false)
.PHONY: testall
.PHONY: setup
setup: Jamstash-master
@which goconvey || (echo "Installing GoConvey" && GO111MODULE=off go get -u github.com/smartystreets/goconvey)
@which wire || (echo "Installing Wire" && GO111MODULE=off go get -u github.com/google/wire/cmd/wire)
update-snapshots: check_go_env
UPDATE_SNAPSHOTS=true ginkgo ./server/subsonic/...
.PHONY: update-snapshots
migration:
@if [ -z "${name}" ]; then echo "Usage: make create-migration name=name_of_migration_file"; exit 1; fi
goose -dir db/migration create ${name}
.PHONY: migration
setup:
@which go-bindata || (echo "Installing BinData" && GO111MODULE=off go get -u github.com/go-bindata/go-bindata/...)
@which reflex || (echo "Installing Reflex" && GO111MODULE=off go get -u github.com/cespare/reflex)
@which goreman || (echo "Installing Goreman" && GO111MODULE=off go get -u github.com/mattn/goreman)
@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)
go mod download
@(cd ./ui && npm ci)
.PHONY: setup
.PHONY: static
static:
cd static && go-bindata -fs -prefix "static" -nocompress -ignore="\\\*.go" -pkg static .
setup-dev: setup setup-git
@which wire || (echo "Installing Wire" && GO111MODULE=off go get -u github.com/google/wire/cmd/wire)
@which ginkgo || (echo "Installing Ginkgo" && GO111MODULE=off go get -u github.com/onsi/ginkgo/ginkgo)
@which goose || (echo "Installing Goose" && GO111MODULE=off go get -u github.com/pressly/goose/cmd/goose)
@which reflex || (echo "Installing Reflex" && GO111MODULE=off go get -u github.com/cespare/reflex)
@which golangci-lint || (echo "Installing GolangCI-Lint" && cd .. && GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.27.0)
@which goimports || (echo "Installing goimports" && GO111MODULE=off go get -u golang.org/x/tools/cmd/goimports)
.PHONY: setup-dev
setup-git:
@mkdir -p .git/hooks
(cd .git/hooks && ln -sf ../../git/* .)
.PHONY: setup-git
Jamstash-master:
wget -N https://github.com/tsquillario/Jamstash/archive/master.zip
unzip -o master.zip
rm master.zip
(cd Jamstash-master && npm ci && npx bower install && npx grunt build)
rm -rf Jamstash-master/node_modules Jamstash-master/bower_components
.PHONE: check_env
check_env: check_go_env check_node_env
.PHONY: check_env
.PHONY: check_go_env
check_go_env:
@(hash go) || (echo "\nERROR: GO environment not setup properly!\n"; exit 1)
@go version | grep -q $(GO_VERSION) || (echo "\nERROR: Please upgrade your GO version\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
.PHONY: check_node_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
.PHONY: build
build: check_go_env
go build -ldflags="-X github.com/deluan/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/deluan/navidrome/consts.gitTag=master"
go build -ldflags="-X github.com/deluan/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/deluan/navidrome/consts.gitTag=$(GIT_TAG)-SNAPSHOT"
.PHONY: build
.PHONY: buildall
buildall: check_env
@(cd ./ui && npm run build)
go-bindata -fs -prefix "resources" -tags embed -ignore="\\\*.go" -pkg resources -o resources/embedded_gen.go resources/...
go-bindata -fs -prefix "ui/build" -tags embed -nocompress -pkg assets -o assets/embedded_gen.go ui/build/...
go build -ldflags="-X github.com/deluan/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/deluan/navidrome/consts.gitTag=master" -tags=embed
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: release
release:
@if [[ ! "${V}" =~ ^[0-9]+\.[0-9]+\.[0-9]+.*$$ ]]; then echo "Usage: make release V=X.X.X"; exit 1; fi
go mod tidy
@@ -76,7 +100,8 @@ release:
make test
git tag v${V}
git push origin v${V}
.PHONY: release
.PHONY: dist
dist:
docker run -it -v $(PWD):/github/workspace -w /github/workspace bepsays/ci-goreleaser:1.13-4 goreleaser release --rm-dist --skip-publish --snapshot
snapshot:
docker run -it -v $(PWD):/workspace -w /workspace deluan/ci-goreleaser:1.14.4-2 goreleaser release --rm-dist --skip-publish --snapshot
.PHONY: snapshot

159
README.md
View File

@@ -1,132 +1,65 @@
# Navidrome Music Streamer
[![Build](https://img.shields.io/github/workflow/status/deluan/navidrome/Build?style=for-the-badge)](https://github.com/deluan/navidrome/actions)
[![Last Release](https://img.shields.io/github/v/release/deluan/navidrome?label=latest&style=for-the-badge)](https://github.com/deluan/navidrome/releases)
[![Docker Pulls](https://img.shields.io/docker/pulls/deluan/navidrome?style=for-the-badge)](https://hub.docker.com/r/deluan/navidrome)
[![Join the Chat](https://img.shields.io/discord/671335427726114836?style=for-the-badge)](https://discord.gg/xh7j7yF)
[![Last Release](https://img.shields.io/github/v/release/deluan/navidrome?label=latest&style=flat-square)](https://github.com/deluan/navidrome/releases)
[![Build](https://img.shields.io/github/workflow/status/deluan/navidrome/Build?style=flat-square)](https://github.com/deluan/navidrome/actions)
[![Downloads](https://img.shields.io/github/downloads/deluan/navidrome/total)](https://github.com/deluan/navidrome/releases/latest)
[![Docker Pulls](https://img.shields.io/docker/pulls/deluan/navidrome?style=flat-square)](https://hub.docker.com/r/deluan/navidrome)
[![Dev Chat](https://img.shields.io/discord/671335427726114836?label=chat&style=flat-square)](https://discord.gg/xh7j7yF)
[![Subreddit](https://img.shields.io/reddit/subreddit-subscribers/navidrome?style=flat-square)](https://www.reddit.com/r/navidrome/)
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0-ff69b4.svg)](code_of_conduct.md)
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 chat in our [Discord server](https://discord.gg/xh7j7yF)
please fill a [GitHub issue](https://github.com/deluan/navidrome/issues) or join the discussion in our
[Subreddit](https://www.reddit.com/r/navidrome/). If you want to contribute to the project in any other way
([ui/backend dev](https://www.navidrome.org/docs/developers/),
[translations](https://www.navidrome.org/docs/developers/translations/),
[themes](https://www.navidrome.org/docs/developers/creating-themes)), please join the chat in our
[Discord server](https://discord.gg/xh7j7yF).
## Demo Site
## Features
- Handles very large music collections
- Streams virtually any audio format available
- Reads and uses all your beautifully curated metadata (id3 tags)
- Multi-user, each user has their own play counts, playlists, favourites, etc..
- Very low resource usage: Ex: with a library of 300GB (~29000 songs), it uses less than 50MB of RAM
- Multi-platform, runs on macOS, Linux and Windows. Docker images are also provided
- Automatically monitors your library for changes, importing new files and reloading new metadata
- Modern and responsive Web interface based on Material UI, to manage users and browse your library
- Compatible with the huge selection of clients for [Subsonic](http://www.subsonic.org),
[Airsonic](https://airsonic.github.io/) and [Madsonic](https://www.madsonic.org/).
See the [complete list of available mobile and web apps](https://airsonic.github.io/docs/apps/)
- Transcoding/Downsampling on-the-fly (WIP. Experimental support is available)
- Integrated music player (WIP)
## Road map
This project is being actively worked on. Expect a more polished experience and new features/releases
on a frequent basis. Some upcoming features planned:
- Last.FM integration
- Pre-build binaries for Raspberry Pi
- Smart/dynamic playlists (similar to iTunes)
- Support for audiobooks (bookmarking)
- Jukebox mode
- Sharing links to albums/songs/playlists
- Podcasts
To see Navidrome in action, check out our [live demo](https://www.navidrome.org/demo/)
## Installation
Various options are available:
See instructions in the [project's website](https://www.navidrome.org/docs/installation/)
### Pre-built executables
## 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 **Raspberry Pi** binaries and Docker images available
- 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**
Just head to the [releases page](https://github.com/deluan/navidrome/releases) and download the latest version for you
platform. There are builds available for Linux, macOS and Windows (32 and 64 bits).
## Documentation
All documentation can be found in the project's website: https://www.navidrome.org/docs.
Here are some useful direct links:
Remember to install [ffmpeg](https://ffmpeg.org/download.html) in your system, a requirement for Navidrome to work properly.
You may find the latest static build for your platform here: https://johnvansickle.com/ffmpeg/
If you have any issues with these binaries, or need a binary for a different platform, please
[open an issue](https://github.com/deluan/navidrome/issues)
### Docker
[Docker images](https://hub.docker.com/r/deluan/navidrome) are available. They include everything needed to run Navidrome. Example of usage:
```yaml
# This is just an example. Customize it to your needs.
version: "3"
services:
navidrome:
image: deluan/navidrome:latest
ports:
- "4533:4533"
environment:
# All options with their default values:
ND_MUSICFOLDER: /music
ND_DATAFOLDER: /data
ND_SCANINTERVAL: 1m
ND_LOGLEVEL: info
ND_PORT: 4533
volumes:
- "./data:/data"
- "/path/to/your/music/folder:/music"
```
To get the cutting-edge, latest version from master, use the image `deluan/navidrome:develop`
### Build from source
You will need to install [Go 1.13](https://golang.org/dl/) and [Node 13.7.0](http://nodejs.org).
You'll also need [ffmpeg](https://ffmpeg.org) installed in your system. The setup is very strict, and
the steps bellow only work with these specific versions (enforced in the Makefile)
After the prerequisites above are installed, clone this repository and build the application with:
```shell script
$ git clone https://github.com/deluan/navidrome
$ cd navidrome
$ make setup # Install tools required for Navidrome's development
$ make buildall # Build UI and server, generates a single executable
```
This will generate the `navidrome` executable binary in the project's root folder.
### Running for the first time
Start the server with:
```shell script
./navidrome
```
The server should start listening for requests on the default port __4533__
After starting Navidrome for the first time, go to http://localhost:4533. It will ask you to create your first admin
user.
For more options, run `navidrome --help`
- [Overview](https://www.navidrome.org/docs/overview/)
- [Installation](https://www.navidrome.org/docs/installation/)
- [Docker](https://www.navidrome.org/docs/installation/docker/)
- [Binaries](https://www.navidrome.org/docs/installation/pre-built-binaries/)
- [Build from source](https://www.navidrome.org/docs/installation/build-from-source/)
- [Development](https://www.navidrome.org/docs/developers/)
- [Subsonic API Compatibility](https://www.navidrome.org/docs/developers/subsonic-api/)
## Screenshots
<p align="center">
<p float="left">
<img width="270" src="https://raw.githubusercontent.com/deluan/navidrome/master/.github/screenshots/ss-mobile-login.png">
<img width="270" src="https://raw.githubusercontent.com/deluan/navidrome/master/.github/screenshots/ss-mobile-player.png">
<img width="270" src="https://raw.githubusercontent.com/deluan/navidrome/master/.github/screenshots/ss-mobile-album-view.png">
<img width="900"src="https://raw.githubusercontent.com/deluan/navidrome/master/.github/screenshots/ss-desktop-player.png">
<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">
</p>
</p>
## Subsonic API Version Compatibility
Check the up to date [compatibility table](https://github.com/deluan/navidrome/blob/master/API_COMPATIBILITY.md)
for the latest Subsonic features available.

View File

@@ -6,7 +6,6 @@ import (
"net/http"
"sync"
"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/log"
)
@@ -14,7 +13,7 @@ var once sync.Once
func AssetFile() http.FileSystem {
once.Do(func() {
log.Warn("Using external assets from " + consts.UIAssetsLocalPath)
log.Warn("Using external assets from 'ui/build' folder")
})
return http.Dir(consts.UIAssetsLocalPath)
return http.Dir("ui/build")
}

View File

@@ -1,98 +0,0 @@
#!/bin/bash
# Script to transfort .itc files into images (JPG or PNG)
#
# .itc files are located in ~/Music/iTunes/Album Artwork
#
# This script uses (/!\ needs ) ImageMagick's convert, hexdump, printf and dd.
#
# This script might be a little slow, You might want to look at Simon Kennedy's work at http://www.sffjunkie.co.uk/python-itc.html
#
# ~/{Library Path}/Album Artwork/Cache/D989408F65D05F99/04/13/04/D989408F65D05F99-EB5B7A9086F4B4D4.itc
#
# The filenames are an amalgam of the library ID (D989408F65D05F99) and the track's ID (EB5B7A9086F4B4D4).
# The directory structure comes from the library ID and the last three digits of the track's ID converted to decimal,
# ie 4D4 becomes 04, 13, 04.
#
AlbumArtwork="${HOME}/Music/iTunes 1/Album Artwork"
DestinationDir="Artwork"
IFS=$'\n'
if [ ! -d "$DestinationDir" ]; then
mkdir "$DestinationDir"
echo "new Images dir"
fi
for file in `find "$AlbumArtwork" -name '*.itc'`; do
start=0x11C
exit=0;
i=1;
echo $file
while [ 1 ]; do
typeOffset=$(($start+0x30))
imageType=$(hexdump -n 4 -s $typeOffset -e '"0x"4/1 "%02x" "\n"' $file)
#If there is no next byte, jump to the next itc file.
if [[ -z $imageType ]]; then
break
fi
imageOffsetOffset=$(($start+8))
itemSize=$(hexdump -n 4 -s $start -e '"0x"4/1 "%02x" "\n"' $file)
imageOffset=$(hexdump -n 4 -s $imageOffsetOffset -e '"0x"4/1 "%02x" "\n"' $file)
imageStart=$(($start+$imageOffset))
imageSize=$(($itemSize-imageOffset))
imageWidth=$(hexdump -n 4 -s $(($start+56)) -e '"0x"4/1 "%02x" "\n"' $file)
imageWidth=$(printf "%d" $imageWidth)
imageHeight=$(hexdump -n 4 -s $(($start+60)) -e '"0x"4/1 "%02x" "\n"' $file)
imageHeight=$(printf "%d" $imageHeight)
dir=$(dirname "$file")
xbase=${file##*/} #file.etc
xpref=${xbase%.*} #file prefix
#echo $file
#echo itemsize $itemSize
#echo start $start
#echo imageOffset $imageOffset
#echo imageStart $imageStart
#echo imageSize $imageSize
#echo imageWidth $imageWidth
#echo imageHeight $imageHeight
if [[ $imageType -eq 0x504E4766 ]] || [[ $imageType -eq 0x0000000E ]] ; then
targetFile="$DestinationDir/$xpref-$i.png"
if [ ! -f "$targetFile" ]; then
echo PNG
dd skip=$imageStart count=$imageSize if="$file" of="$targetFile" bs=1 &> /dev/null
fi
elif [[ $imageType -eq 0x41524762 ]] ; then
targetFile="$DestinationDir/$xpref-$i.png"
if [ ! -f "$targetFile" ]; then
echo ARGB
dd skip=$imageStart count=$imageSize if="$file" of="$TMPDIR/test$i" bs=1 &> /dev/null
#Using a matrix to convert ARGB to RGBA since imagemagick does only support rgba input
convert -size $imageWidth"x"$imageHeight -depth 8 -color-matrix '0 1 0 0 0 0 1 0 0 0 0 1 1 0 0 0' rgba:"$TMPDIR/test$i" "$targetFile"
fi
elif [[ $imageType -eq 0x0000000D ]] ; then
targetFile="$DestinationDir/$xpref-$i.jpg"
if [ ! -f "$targetFile" ]; then
echo JPG
dd skip=$imageStart count=$imageSize if="$file" of="$targetFile" bs=1 &> /dev/null
fi
else
echo $imageType
exit=1
break;
fi
start=$(($start+$itemSize))
i=$(($i+1))
done
done

View File

@@ -1,14 +0,0 @@
#!/usr/bin/env bash
gofmtcmd=`which goimports || echo "gofmt"`
gofiles=$(git diff --name-only --diff-filter=ACM | grep '.go$')
[ -z "$gofiles" ] && exit 0
unformatted=`$gofmtcmd -l $gofiles`
[ -z "$unformatted" ] && exit 0
for f in $unformatted; do
$gofmtcmd -w -l "$f"
gofmt -s -w -l "$f"
done

96
cmd/root.go Normal file
View File

@@ -0,0 +1,96 @@
package cmd
import (
"fmt"
"os"
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/db"
"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) {
startServer()
},
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 startServer() {
db.EnsureLatestVersion()
subsonic, err := CreateSubsonicAPIRouter()
if err != nil {
panic(fmt.Sprintf("Could not create the Subsonic API router. Aborting! err=%v", err))
}
a := CreateServer(conf.Server.MusicFolder)
a.MountRouter(consts.URLPathSubsonicAPI, subsonic)
a.MountRouter(consts.URLPathUI, CreateAppRouter())
a.Run(fmt.Sprintf("%s:%d", conf.Server.Address, conf.Server.Port))
}
// TODO: Implemement some struct tags to map flags to viper
func 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")
_ = 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/deluan/navidrome/conf"
"github.com/deluan/navidrome/log"
"github.com/spf13/cobra"
)
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() {
scanner := CreateScanner(conf.Server.MusicFolder)
err := scanner.RescanAll(fullRescan)
if err != nil {
log.Error("Error scanning media folder", "folder", conf.Server.MusicFolder, err)
}
if fullRescan {
log.Info("Finished full rescan")
} else {
log.Info("Finished rescan")
}
}

View File

@@ -3,9 +3,11 @@
//go:generate wire
//+build !wireinject
package main
package cmd
import (
"github.com/deluan/navidrome/core"
"github.com/deluan/navidrome/core/transcoder"
"github.com/deluan/navidrome/engine"
"github.com/deluan/navidrome/persistence"
"github.com/deluan/navidrome/scanner"
@@ -24,16 +26,23 @@ func CreateServer(musicFolder string) *server.Server {
return serverServer
}
func CreateAppRouter(path string) *app.Router {
func CreateScanner(musicFolder string) *scanner.Scanner {
dataStore := persistence.New()
router := app.New(dataStore, path)
scannerScanner := scanner.New(dataStore)
return scannerScanner
}
func CreateAppRouter() *app.Router {
dataStore := persistence.New()
router := app.New(dataStore)
return router
}
func CreateSubsonicAPIRouter() *subsonic.Router {
func CreateSubsonicAPIRouter() (*subsonic.Router, error) {
dataStore := persistence.New()
browser := engine.NewBrowser(dataStore)
cover := engine.NewCover(dataStore)
artworkCache := core.NewImageCache()
artwork := core.NewArtwork(dataStore, artworkCache)
nowPlayingRepository := engine.NewNowPlayingRepository()
listGenerator := engine.NewListGenerator(dataStore, nowPlayingRepository)
users := engine.NewUsers(dataStore)
@@ -41,11 +50,14 @@ func CreateSubsonicAPIRouter() *subsonic.Router {
ratings := engine.NewRatings(dataStore)
scrobbler := engine.NewScrobbler(dataStore, nowPlayingRepository)
search := engine.NewSearch(dataStore)
mediaStreamer := engine.NewMediaStreamer(dataStore)
router := subsonic.New(browser, cover, listGenerator, users, playlists, ratings, scrobbler, search, mediaStreamer)
return router
transcoderTranscoder := transcoder.New()
transcodingCache := core.NewTranscodingCache()
mediaStreamer := core.NewMediaStreamer(dataStore, transcoderTranscoder, transcodingCache)
players := engine.NewPlayers(dataStore)
router := subsonic.New(browser, artwork, listGenerator, users, playlists, ratings, scrobbler, search, mediaStreamer, players, dataStore)
return router, nil
}
// wire_injectors.go:
var allProviders = wire.NewSet(engine.Set, scanner.New, subsonic.New, app.New, persistence.New)
var allProviders = wire.NewSet(engine.Set, core.Set, scanner.New, subsonic.New, app.New, persistence.New)

View File

@@ -1,8 +1,9 @@
//+build wireinject
package main
package cmd
import (
"github.com/deluan/navidrome/core"
"github.com/deluan/navidrome/engine"
"github.com/deluan/navidrome/persistence"
"github.com/deluan/navidrome/scanner"
@@ -14,6 +15,7 @@ import (
var allProviders = wire.NewSet(
engine.Set,
core.Set,
scanner.New,
subsonic.New,
app.New,
@@ -27,10 +29,16 @@ func CreateServer(musicFolder string) *server.Server {
))
}
func CreateAppRouter(path string) *app.Router {
func CreateScanner(musicFolder string) *scanner.Scanner {
panic(wire.Build(
allProviders,
))
}
func CreateAppRouter() *app.Router {
panic(wire.Build(allProviders))
}
func CreateSubsonicAPIRouter() *subsonic.Router {
func CreateSubsonicAPIRouter() (*subsonic.Router, error) {
panic(wire.Build(allProviders))
}

View File

@@ -1,97 +1,131 @@
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/spf13/viper"
)
type nd struct {
Port string `default:"4533"`
MusicFolder string `default:"./music"`
DataFolder string `default:"./"`
DbPath string
LogLevel string `default:"info"`
type configOptions struct {
ConfigFile string
Address string
Port int
MusicFolder string
DataFolder string
DbPath string
LogLevel string
ScanInterval time.Duration
SessionTimeout time.Duration
BaseURL string
UILoginBackgroundURL string
EnableTranscodingConfig bool
TranscodingCacheSize string
ImageCacheSize 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]([)"`
EnableDownsampling bool `default:"false"`
MaxBitRate int `default:"0"`
DownsampleCommand string `default:"ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -f mp3 -"`
ProbeCommand string `default:"ffmpeg -i %s -f ffmetadata"`
ScanInterval string `default:"1m"`
IgnoredArticles string
IndexGroups string
ProbeCommand string
CoverArtPriority string
CoverJpegQuality int
UIWelcomeMessage string
GATrackingID string
AuthRequestLimit int
AuthWindowLength time.Duration
// DevFlags. These are used to enable/disable debugging and incomplete features
DevDisableBanner bool `default:"false"`
DevLogSourceLine bool `default:"false"`
DevAutoCreateAdminPassword string `default:""`
DevLogSourceLine bool
DevAutoCreateAdminPassword string
}
var Server = &nd{}
var Server = &configOptions{}
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
}
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, "navidrome.db")
}
if os.Getenv("PORT") != "" {
Server.Port = os.Getenv("PORT")
}
log.SetLevelString(Server.LogLevel)
log.SetLogSourceLine(Server.DevLogSourceLine)
log.Trace("Loaded configuration", "file", confFile, "config", fmt.Sprintf("%#v", Server))
func LoadFromFile(confFile string) {
viper.SetConfigFile(confFile)
Load()
}
func Load() {
LoadFromFile(consts.LocalConfigFile)
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.Debug("Loaded configuration", "file", Server.ConfigFile, "config", fmt.Sprintf("%#v", Server))
}
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", time.Minute)
viper.SetDefault("baseurl", "")
viper.SetDefault("uiloginbackgroundurl", "https://source.unsplash.com/random/1600x900?music")
viper.SetDefault("enabletranscodingconfig", false)
viper.SetDefault("transcodingcachesize", "100MB")
viper.SetDefault("imagecachesize", "100MB")
// Config options only valid for file/env configuration
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("gatrackingid", "")
viper.SetDefault("authrequestlimit", 5)
viper.SetDefault("authwindowlength", 20*time.Second)
// DevFlags. These are used to enable/disable debugging and incomplete features
viper.SetDefault("devlogsourceline", false)
viper.SetDefault("devautocreateadminpassword", "")
viper.SetDefault("devoldscanner", false)
}
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")
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

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

View File

@@ -1,19 +1,69 @@
package consts
import "time"
import (
"crypto/md5"
"fmt"
"strings"
"time"
)
const (
AppName = "navidrome"
LocalConfigFile = "./navidrome.toml"
DefaultDbPath = "navidrome.db?cache=shared&_busy_timeout=15000&_journal_mode=WAL&_foreign_keys=on"
InitialSetupFlagKey = "InitialSetup"
JWTSecretKey = "JWTSecret"
JWTIssuer = "ND"
JWTTokenExpiration = 30 * time.Minute
UIAssetsLocalPath = "ui/build"
UIAuthorizationHeader = "X-ND-Authorization"
JWTSecretKey = "JWTSecret"
JWTIssuer = "ND"
DefaultSessionTimeout = 24 * time.Hour
DevInitialUserName = "admin"
DevInitialName = "Dev Admin"
URLPathUI = "/app"
URLPathSubsonicAPI = "/rest"
RequestThrottleBacklogLimit = 100
RequestThrottleBacklogTimeout = time.Minute
I18nFolder = "i18n"
SkipScanFile = ".ndignore"
PlaceholderAlbumArt = "navidrome-600x600.png"
)
// Cache options
const (
TranscodingCacheDir = "cache/transcoding"
DefaultTranscodingCacheMaxItems = 0 // Unlimited
ImageCacheDir = "cache/images"
DefaultImageCacheMaxItems = 0 // Unlimited
DefaultCacheSize = 100 * 1024 * 1024 // 100MB
DefaultCacheCleanUpInterval = 10 * time.Minute
)
var (
DefaultTranscodings = []map[string]interface{}{
{
"name": "mp3 audio",
"targetFormat": "mp3",
"defaultBitRate": 192,
"command": "ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -f mp3 -",
},
{
"name": "opus audio",
"targetFormat": "oga",
"defaultBitRate": 128,
"command": "ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -c:a libopus -f opus -",
},
}
)
var (
VariousArtists = "Various Artists"
VariousArtistsID = fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(VariousArtists))))
UnknownArtist = "[Unknown Artist]"
)

View File

@@ -8,7 +8,6 @@ func init() {
".ogg": "audio/ogg",
".oga": "audio/ogg",
".opus": "audio/ogg",
".ogx": "application/ogg",
".aac": "audio/mp4",
".m4a": "audio/mp4",
".m4b": "audio/mp4",
@@ -18,20 +17,11 @@ func init() {
".ape": "audio/x-monkeys-audio",
".mpc": "audio/x-musepack",
".shn": "audio/x-shn",
".flv": "video/x-flv",
".avi": "video/avi",
".mpg": "video/mpeg",
".mpeg": "video/mpeg",
".mp4": "video/mp4",
".m4v": "video/x-m4v",
".mkv": "video/x-matroska",
".mov": "video/quicktime",
".wmv": "video/x-ms-wmv",
".ogv": "video/ogg",
".divx": "video/divx",
".m2ts": "video/MP2T",
".ts": "video/MP2T",
".webm": "video/webm",
".aif": "audio/x-aiff",
".aiff": "audio/x-aiff",
".m3u": "audio/x-mpegurl",
".pls": "audio/x-scpls",
".dsf": "audio/dsd",
".gif": "image/gif",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",

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

15
contrib/navidrome Normal file
View File

@@ -0,0 +1,15 @@
#!/sbin/openrc-run
name=$RC_SVCNAME
command="/opt/navidrome/${RC_SVCNAME}"
command_args="-datafolder /opt/navidrome"
command_user="${RC_SVCNAME}"
pidfile="/var/run/${RC_SVCNAME}.pid"
output_log="/opt/navidrome/${RC_SVCNAME}.log"
error_log="/opt/navidrome/${RC_SVCNAME}.err"
command_background="yes"
depend() {
need net
}

48
contrib/navidrome.service Normal file
View File

@@ -0,0 +1,48 @@
# This file ususaly goes in /etc/systemd/system
[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
[Service]
User=navidrome
Group=navidrome
Type=simple
ExecStart=/usr/bin/navidrome
WorkingDirectory=/var/lib/navidrome
TimeoutStopSec=20
KillMode=process
Restart=on-failure
EnvironmentFile=-/etc/sysconfig/navidrome
# See https://www.freedesktop.org/software/systemd/man/systemd.exec.html
DevicePolicy=closed
NoNewPrivileges=yes
PrivateTmp=yes
PrivateUsers=yes
ProtectControlGroups=yes
ProtectKernelModules=yes
ProtectKernelTunables=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
# You can uncomment the following line if you're not using the jukebox This
# will prevent navidrome from accessing any real (physical) devices
#PrivateDevices=yes
# You can change the following line to `strict` instead of `full` if you don't
# want navidrome to be able to write anything on your filesystem outside of
# /var/lib/navidrome.
ProtectSystem=full
# You can comment the following line if you don't have any media in /home/*.
# This will prevent navidrome from ever reading/writing anything there.
ProtectHome=true

198
core/artwork.go Normal file
View File

@@ -0,0 +1,198 @@
package core
import (
"bytes"
"context"
"errors"
"fmt"
"image"
_ "image/gif"
"image/jpeg"
_ "image/png"
"io"
"os"
"strings"
"time"
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/resources"
"github.com/deluan/navidrome/utils"
"github.com/dhowden/tag"
"github.com/disintegration/imaging"
)
type Artwork interface {
Get(ctx context.Context, id string, size int, out io.Writer) error
}
type ArtworkCache FileCache
func NewArtwork(ds model.DataStore, cache ArtworkCache) Artwork {
return &artwork{ds: ds, cache: cache}
}
type artwork struct {
ds model.DataStore
cache FileCache
}
type imageInfo struct {
c *artwork
path string
size int
lastUpdate time.Time
}
func (ci *imageInfo) String() string {
return fmt.Sprintf("%s.%d.%s.%d", ci.path, ci.size, ci.lastUpdate.Format(time.RFC3339Nano), conf.Server.CoverJpegQuality)
}
func (c *artwork) Get(ctx context.Context, id string, size int, out io.Writer) error {
path, lastUpdate, err := c.getImagePath(ctx, id)
if err != nil && err != model.ErrNotFound {
return err
}
info := &imageInfo{
c: c,
path: path,
size: size,
lastUpdate: lastUpdate,
}
r, err := c.cache.Get(ctx, info)
if err != nil {
log.Error(ctx, "Error accessing image cache", "path", path, "size", size, err)
return err
}
_, err = io.Copy(out, r)
return err
}
func (c *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 = c.ds.Album(ctx).Get(id)
if err != nil {
return
}
if al.CoverArtId == "" {
err = model.ErrNotFound
}
return al.CoverArtPath, al.UpdatedAt, err
}
log.Trace(ctx, "Looking for media file art", "id", id)
// if id is a mediafile cover id
var mf *model.MediaFile
mf, err = c.ds.MediaFile(ctx).Get(id)
if err != nil {
return
}
if mf.HasCoverArt {
return mf.Path, mf.UpdatedAt, nil
}
// if the mediafile does not have a coverArt, fallback to the album cover
log.Trace(ctx, "Media file does not contain art. Falling back to album art", "id", id, "albumId", "al-"+mf.AlbumID)
return c.getImagePath(ctx, "al-"+mf.AlbumID)
}
func (c *artwork) getArtwork(ctx context.Context, path string, size int) (reader io.Reader, err error) {
defer func() {
if err != nil {
log.Warn(ctx, "Error extracting image", "path", path, "size", size, err)
reader, err = resources.AssetFile().Open(consts.PlaceholderAlbumArt)
}
}()
if path == "" {
return nil, errors.New("empty path given for artwork")
}
var data []byte
if utils.IsAudioFile(path) {
data, err = readFromTag(path)
} else {
data, err = readFromFile(path)
}
if err != nil {
return
} else if size > 0 {
data, err = resizeImage(bytes.NewReader(data), size)
}
// Confirm the image is valid. Costly, but necessary
_, _, err = image.Decode(bytes.NewReader(data))
if err == nil {
reader = bytes.NewReader(data)
}
return
}
func resizeImage(reader io.Reader, size int) ([]byte, error) {
img, _, err := image.Decode(reader)
if err != nil {
return nil, err
}
m := imaging.Resize(img, size, size, imaging.Lanczos)
buf := new(bytes.Buffer)
err = jpeg.Encode(buf, m, &jpeg.Options{Quality: conf.Server.CoverJpegQuality})
return buf.Bytes(), err
}
func readFromTag(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
m, err := tag.ReadFrom(f)
if err != nil {
return nil, err
}
picture := m.Picture()
if picture == nil {
return nil, errors.New("file does not contain embedded art")
}
return picture.Data, nil
}
func readFromFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var buf bytes.Buffer
if _, err := buf.ReadFrom(f); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func NewImageCache() ArtworkCache {
return NewFileCache("Image", conf.Server.ImageCacheSize, consts.ImageCacheDir, consts.DefaultImageCacheMaxItems,
func(ctx context.Context, arg fmt.Stringer) (io.Reader, error) {
info := arg.(*imageInfo)
reader, err := info.c.getArtwork(ctx, 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
})
}

132
core/artwork_test.go Normal file
View File

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

View File

@@ -1,10 +1,12 @@
package auth
import (
"context"
"fmt"
"sync"
"time"
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
@@ -13,14 +15,15 @@ import (
)
var (
once sync.Once
JwtSecret []byte
TokenAuth *jwtauth.JWTAuth
once sync.Once
JwtSecret []byte
TokenAuth *jwtauth.JWTAuth
sessionTimeOut time.Duration
)
func InitTokenAuth(ds model.DataStore) {
once.Do(func() {
secret, err := ds.Property(nil).DefaultGet(consts.JWTSecretKey, "not so secret")
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)
}
@@ -39,8 +42,17 @@ func CreateToken(u *model.User) (string, error) {
return TouchToken(token)
}
func getSessionTimeOut() time.Duration {
if sessionTimeOut == 0 {
sessionTimeOut = conf.Server.SessionTimeout
log.Info("Setting Session Timeout", "value", sessionTimeOut)
}
return sessionTimeOut
}
func TouchToken(token *jwt.Token) (string, error) {
expireIn := time.Now().Add(consts.JWTTokenExpiration).Unix()
timeout := getSessionTimeOut()
expireIn := time.Now().Add(timeout).Unix()
claims := token.Claims.(jwt.MapClaims)
claims["exp"] = expireIn

View File

@@ -4,7 +4,7 @@ import (
"testing"
"time"
"github.com/deluan/navidrome/engine/auth"
"github.com/deluan/navidrome/core/auth"
"github.com/deluan/navidrome/log"
"github.com/dgrijalva/jwt-go"
. "github.com/onsi/ginkgo"

15
core/common.go Normal file
View File

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

17
core/core_suite_test.go Normal file
View File

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

195
core/file_caches.go Normal file
View File

@@ -0,0 +1,195 @@
package core
import (
"context"
"fmt"
"io"
"path/filepath"
"sync"
"time"
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/log"
"github.com/djherbis/fscache"
"github.com/dustin/go-humanize"
)
type ReadFunc func(ctx context.Context, arg fmt.Stringer) (io.Reader, error)
type FileCache interface {
Get(ctx context.Context, arg fmt.Stringer) (*CachedStream, error)
Ready() bool
}
func NewFileCache(name, cacheSize, cacheFolder string, maxItems int, getReader ReadFunc) *fileCache {
fc := &fileCache{
name: name,
cacheSize: cacheSize,
cacheFolder: filepath.FromSlash(cacheFolder),
maxItems: maxItems,
getReader: getReader,
mutex: &sync.RWMutex{},
}
go func() {
cache, err := newFSCache(fc.name, fc.cacheSize, fc.cacheFolder, fc.maxItems)
fc.mutex.Lock()
defer fc.mutex.Unlock()
if err == nil {
fc.cache = cache
fc.disabled = cache == nil
}
fc.ready = true
if fc.disabled {
log.Debug("Cache disabled", "cache", fc.name, "size", fc.cacheSize)
}
}()
return fc
}
type fileCache struct {
name string
cacheSize string
cacheFolder string
maxItems int
cache fscache.Cache
getReader ReadFunc
disabled bool
ready bool
mutex *sync.RWMutex
}
func (fc *fileCache) Ready() bool {
fc.mutex.RLock()
defer fc.mutex.RUnlock()
return fc.ready
}
func (fc *fileCache) available(ctx context.Context) bool {
fc.mutex.RLock()
defer fc.mutex.RUnlock()
if !fc.ready {
log.Debug(ctx, "Cache not initialized yet", "cache", fc.name)
}
return fc.ready && !fc.disabled
}
func (fc *fileCache) Get(ctx context.Context, arg fmt.Stringer) (*CachedStream, error) {
if !fc.available(ctx) {
reader, err := fc.getReader(ctx, arg)
if err != nil {
return nil, err
}
return &CachedStream{Reader: reader}, nil
}
key := arg.String()
r, w, err := fc.cache.Get(key)
if err != nil {
return nil, err
}
cached := w == nil
if !cached {
log.Trace(ctx, "Cache MISS", "cache", fc.name, "key", key)
reader, err := fc.getReader(ctx, arg)
if err != nil {
return nil, err
}
go copyAndClose(ctx, w, reader)
}
// 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.Trace(ctx, "Cache HIT", "cache", fc.name, "key", key, "size", size)
sr := io.NewSectionReader(r, 0, size)
return &CachedStream{
Reader: sr,
Seeker: sr,
Cached: true,
}, nil
} else {
log.Trace(ctx, "Cache HIT", "cache", fc.name, "key", key)
}
}
// All other cases, just return a Reader, without Seek capabilities
return &CachedStream{Reader: r, Cached: cached}, nil
}
type CachedStream struct {
io.Reader
io.Seeker
Cached bool
}
func (s *CachedStream) Seekable() bool { return s.Seeker != nil }
func (s *CachedStream) Close() error {
if c, ok := s.Reader.(io.Closer); ok {
return c.Close()
}
return nil
}
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 copyAndClose(ctx context.Context, w io.WriteCloser, r io.Reader) {
_, err := io.Copy(w, r)
if err != nil {
log.Error(ctx, "Error copying data to cache", err)
}
if c, ok := r.(io.Closer); ok {
err = c.Close()
if err != nil {
log.Error(ctx, "Error closing source stream", err)
}
}
err = w.Close()
if err != nil {
log.Error(ctx, "Error closing cache writer", err)
}
}
func newFSCache(name, cacheSize, cacheFolder string, maxItems int) (fscache.Cache, error) {
size, err := humanize.ParseBytes(cacheSize)
if err != nil {
log.Error("Invalid cache size. Using default size", "cache", name, "size", cacheSize,
"defaultSize", humanize.Bytes(consts.DefaultCacheSize))
size = consts.DefaultCacheSize
}
if size == 0 {
log.Warn(fmt.Sprintf("%s cache disabled", name))
return nil, nil
}
start := time.Now()
lru := fscache.NewLRUHaunter(maxItems, int64(size), consts.DefaultCacheCleanUpInterval)
h := fscache.NewLRUHaunterStrategy(lru)
cacheFolder = filepath.Join(conf.Server.DataFolder, cacheFolder)
log.Info(fmt.Sprintf("Creating %s cache", name), "path", cacheFolder, "maxSize", humanize.Bytes(size))
fs, err := fscache.NewFs(cacheFolder, 0755)
if err != nil {
log.Error(fmt.Sprintf("Error initializing %s cache", name), err, "elapsedTime", time.Since(start))
return nil, err
}
log.Debug(fmt.Sprintf("%s cache initialized", name), "elapsedTime", time.Since(start))
return fscache.NewCacheWithHaunter(fs, h)
}

100
core/file_caches_test.go Normal file
View File

@@ -0,0 +1,100 @@
package core
import (
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/deluan/navidrome/conf"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
// Call NewFileCache and wait for it to be ready
func callNewFileCache(name, cacheSize, cacheFolder string, maxItems int, getReader ReadFunc) *fileCache {
fc := NewFileCache(name, cacheSize, cacheFolder, maxItems, getReader)
Eventually(func() bool { return fc.Ready() }).Should(BeTrue())
return fc
}
var _ = Describe("File Caches", func() {
BeforeEach(func() {
conf.Server.DataFolder, _ = ioutil.TempDir("", "file_caches")
})
AfterEach(func() {
os.RemoveAll(conf.Server.DataFolder)
})
Describe("NewFileCache", func() {
It("creates the cache folder", func() {
Expect(callNewFileCache("test", "1k", "test", 0, nil)).ToNot(BeNil())
_, err := os.Stat(filepath.Join(conf.Server.DataFolder, "test"))
Expect(os.IsNotExist(err)).To(BeFalse())
})
It("creates the cache folder with invalid size", func() {
fc := callNewFileCache("test", "abc", "test", 0, nil)
Expect(fc.cache).ToNot(BeNil())
Expect(fc.disabled).To(BeFalse())
})
It("returns empty if cache size is '0'", func() {
fc := callNewFileCache("test", "0", "test", 0, nil)
Expect(fc.cache).To(BeNil())
Expect(fc.disabled).To(BeTrue())
})
})
Describe("FileCache", func() {
It("caches data if cache is enabled", func() {
called := false
fc := callNewFileCache("test", "1KB", "test", 0, func(ctx context.Context, arg fmt.Stringer) (io.Reader, error) {
called = true
return strings.NewReader(arg.String()), nil
})
// First call is a MISS
s, err := fc.Get(context.TODO(), &testArg{"test"})
Expect(err).To(BeNil())
Expect(s.Cached).To(BeFalse())
Expect(ioutil.ReadAll(s)).To(Equal([]byte("test")))
// Second call is a HIT
called = false
s, err = fc.Get(context.TODO(), &testArg{"test"})
Expect(err).To(BeNil())
Expect(ioutil.ReadAll(s)).To(Equal([]byte("test")))
Expect(s.Cached).To(BeTrue())
Expect(called).To(BeFalse())
})
It("does not cache data if cache is disabled", func() {
called := false
fc := callNewFileCache("test", "0", "test", 0, func(ctx context.Context, arg fmt.Stringer) (io.Reader, error) {
called = true
return strings.NewReader(arg.String()), nil
})
// First call is a MISS
s, err := fc.Get(context.TODO(), &testArg{"test"})
Expect(err).To(BeNil())
Expect(s.Cached).To(BeFalse())
Expect(ioutil.ReadAll(s)).To(Equal([]byte("test")))
// Second call is also a MISS
called = false
s, err = fc.Get(context.TODO(), &testArg{"test"})
Expect(err).To(BeNil())
Expect(ioutil.ReadAll(s)).To(Equal([]byte("test")))
Expect(s.Cached).To(BeFalse())
Expect(called).To(BeTrue())
})
})
})
type testArg struct{ s string }
func (t *testArg) String() string { return t.s }

186
core/media_streamer.go Normal file
View File

@@ -0,0 +1,186 @@
package core
import (
"context"
"fmt"
"io"
"mime"
"os"
"time"
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/core/transcoder"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/model/request"
)
type MediaStreamer interface {
NewStream(ctx context.Context, id string, reqFormat string, reqBitRate int) (*Stream, error)
}
type TranscodingCache FileCache
func NewMediaStreamer(ds model.DataStore, ffm transcoder.Transcoder, cache TranscodingCache) MediaStreamer {
return &mediaStreamer{ds: ds, ffm: ffm, cache: cache}
}
type mediaStreamer struct {
ds model.DataStore
ffm transcoder.Transcoder
cache FileCache
}
type streamJob struct {
ms *mediaStreamer
mf *model.MediaFile
format string
bitRate int
}
func (j *streamJob) String() 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) {
mf, err := ms.ds.MediaFile(ctx).Get(id)
if err != nil {
return nil, err
}
var format string
var bitRate int
var cached bool
defer func() {
log.Info("Streaming file", "title", mf.Title, "artist", mf.Artist, "format", format, "cached", cached,
"bitRate", bitRate, "user", userName(ctx), "transcoding", format != "raw",
"originalFormat", mf.Suffix, "originalBitRate", mf.BitRate)
}()
format, bitRate = selectTranscodingOptions(ctx, ms.ds, mf, reqFormat, reqBitRate)
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,
"requestBitrate", reqBitRate, "requestFormat", reqFormat,
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix,
"selectedBitrate", bitRate, "selectedFormat", format)
f, err := os.Open(mf.Path)
if err != nil {
return nil, err
}
s.ReadCloser = f
s.Seeker = f
s.format = mf.Suffix
return s, nil
}
job := &streamJob{
ms: ms,
mf: mf,
format: format,
bitRate: bitRate,
}
r, err := ms.cache.Get(ctx, job)
if err != nil {
log.Error(ctx, "Error accessing transcoding cache", "id", mf.ID, err)
return nil, err
}
cached = r.Cached
log.Debug(ctx, "Streaming TRANSCODED file", "id", mf.ID, "path", mf.Path,
"requestBitrate", reqBitRate, "requestFormat", reqFormat,
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix,
"selectedBitrate", bitRate, "selectedFormat", format, "cached", cached, "seekable", s.Seekable())
s.ReadCloser = r
if r.Seekable() {
s.Seeker = r
}
return s, nil
}
type Stream struct {
ctx context.Context
mf *model.MediaFile
bitRate int
format string
io.ReadCloser
io.Seeker
}
func (s *Stream) Seekable() bool { return s.Seeker != nil }
func (s *Stream) Duration() float32 { return s.mf.Duration }
func (s *Stream) ContentType() string { return mime.TypeByExtension("." + s.format) }
func (s *Stream) Name() string { return s.mf.Title + "." + s.format }
func (s *Stream) ModTime() time.Time { return s.mf.UpdatedAt }
func (s *Stream) EstimatedContentLength() int {
return int(s.mf.Duration * float32(s.bitRate) / 8 * 1024)
}
// TODO This function deserves some love (refactoring)
func selectTranscodingOptions(ctx context.Context, ds model.DataStore, mf *model.MediaFile, reqFormat string, reqBitRate int) (format string, bitRate int) {
format = "raw"
if reqFormat == "raw" {
return
}
if reqFormat == mf.Suffix && reqBitRate == 0 {
bitRate = mf.BitRate
return
}
trc, hasDefault := request.TranscodingFrom(ctx)
var cFormat string
var cBitRate int
if reqFormat != "" {
cFormat = reqFormat
} else {
if hasDefault {
cFormat = trc.TargetFormat
cBitRate = trc.DefaultBitRate
if p, ok := request.PlayerFrom(ctx); ok {
cBitRate = p.MaxBitRate
}
}
}
if reqBitRate > 0 {
cBitRate = reqBitRate
}
if cBitRate == 0 && cFormat == "" {
return
}
t, err := ds.Transcoding(ctx).FindByFormat(cFormat)
if err == nil {
format = t.TargetFormat
if cBitRate != 0 {
bitRate = cBitRate
} else {
bitRate = t.DefaultBitRate
}
}
if format == mf.Suffix && bitRate >= mf.BitRate {
format = "raw"
bitRate = 0
}
return
}
func NewTranscodingCache() TranscodingCache {
return NewFileCache("Transcoding", conf.Server.TranscodingCacheSize,
consts.TranscodingCacheDir, consts.DefaultTranscodingCacheMaxItems,
func(ctx context.Context, arg fmt.Stringer) (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
})
}

214
core/media_streamer_test.go Normal file
View File

@@ -0,0 +1,214 @@
package core
import (
"context"
"io"
"io/ioutil"
"os"
"strings"
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/model/request"
"github.com/deluan/navidrome/persistence"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("MediaStreamer", func() {
var streamer MediaStreamer
var ds model.DataStore
ffmpeg := &fakeFFmpeg{Data: "fake data"}
ctx := log.NewContext(context.TODO())
BeforeEach(func() {
conf.Server.DataFolder, _ = ioutil.TempDir("", "file_caches")
conf.Server.TranscodingCacheSize = "100MB"
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}]`)
testCache := NewTranscodingCache()
Eventually(func() bool { return testCache.Ready() }).Should(BeTrue())
streamer = NewMediaStreamer(ds, ffmpeg, testCache)
})
AfterEach(func() {
os.RemoveAll(conf.Server.DataFolder)
})
Context("NewStream", func() {
It("returns a seekable stream if format is 'raw'", func() {
s, err := streamer.NewStream(ctx, "123", "raw", 0)
Expect(err).ToNot(HaveOccurred())
Expect(s.Seekable()).To(BeTrue())
})
It("returns a seekable stream if maxBitRate is 0", func() {
s, err := streamer.NewStream(ctx, "123", "mp3", 0)
Expect(err).ToNot(HaveOccurred())
Expect(s.Seekable()).To(BeTrue())
})
It("returns a seekable stream if maxBitRate is higher than file bitRate", func() {
s, err := streamer.NewStream(ctx, "123", "mp3", 320)
Expect(err).ToNot(HaveOccurred())
Expect(s.Seekable()).To(BeTrue())
})
It("returns a NON seekable stream if transcode is required", func() {
s, err := streamer.NewStream(ctx, "123", "mp3", 64)
Expect(err).To(BeNil())
Expect(s.Seekable()).To(BeFalse())
Expect(s.Duration()).To(Equal(float32(257.0)))
})
It("returns a seekable stream if the file is complete in the cache", func() {
s, err := streamer.NewStream(ctx, "123", "mp3", 32)
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())
})
})
Context("selectTranscodingOptions", func() {
mf := &model.MediaFile{}
Context("player is not configured", func() {
It("returns raw if raw is requested", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, _ := selectTranscodingOptions(ctx, ds, mf, "raw", 0)
Expect(format).To(Equal("raw"))
})
It("returns raw if a transcoder does not exists", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, _ := selectTranscodingOptions(ctx, ds, mf, "m4a", 0)
Expect(format).To(Equal("raw"))
})
It("returns the requested format if a transcoder exists", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "mp3", 0)
Expect(format).To(Equal("mp3"))
Expect(bitRate).To(Equal(160)) // Default Bit Rate
})
It("returns raw if requested format is the same as the original and it is not necessary to downsample", func() {
mf.Suffix = "mp3"
mf.BitRate = 112
format, _ := selectTranscodingOptions(ctx, ds, mf, "mp3", 128)
Expect(format).To(Equal("raw"))
})
It("returns the requested format if requested BitRate is lower than original", func() {
mf.Suffix = "mp3"
mf.BitRate = 320
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "mp3", 192)
Expect(format).To(Equal("mp3"))
Expect(bitRate).To(Equal(192))
})
It("returns raw if requested format is the same as the original, but requested BitRate is 0", func() {
mf.Suffix = "mp3"
mf.BitRate = 320
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "mp3", 0)
Expect(format).To(Equal("raw"))
Expect(bitRate).To(Equal(320))
})
})
Context("player has format configured", func() {
BeforeEach(func() {
t := model.Transcoding{ID: "oga1", TargetFormat: "oga", DefaultBitRate: 96}
ctx = request.WithTranscoding(ctx, t)
})
It("returns raw if raw is requested", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, _ := selectTranscodingOptions(ctx, ds, mf, "raw", 0)
Expect(format).To(Equal("raw"))
})
It("returns configured format/bitrate as default", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "", 0)
Expect(format).To(Equal("oga"))
Expect(bitRate).To(Equal(96))
})
It("returns requested format", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "mp3", 0)
Expect(format).To(Equal("mp3"))
Expect(bitRate).To(Equal(160)) // Default Bit Rate
})
It("returns requested bitrate", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "", 80)
Expect(format).To(Equal("oga"))
Expect(bitRate).To(Equal(80))
})
It("returns raw if selected bitrate and format is the same as original", func() {
mf.Suffix = "mp3"
mf.BitRate = 192
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "mp3", 192)
Expect(format).To(Equal("raw"))
Expect(bitRate).To(Equal(0))
})
})
Context("player has maxBitRate configured", func() {
BeforeEach(func() {
t := model.Transcoding{ID: "oga1", TargetFormat: "oga", DefaultBitRate: 96}
p := model.Player{ID: "player1", TranscodingId: t.ID, MaxBitRate: 80}
ctx = request.WithTranscoding(ctx, t)
ctx = request.WithPlayer(ctx, p)
})
It("returns raw if raw is requested", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, _ := selectTranscodingOptions(ctx, ds, mf, "raw", 0)
Expect(format).To(Equal("raw"))
})
It("returns configured format/bitrate as default", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "", 0)
Expect(format).To(Equal("oga"))
Expect(bitRate).To(Equal(80))
})
It("returns requested format", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "mp3", 0)
Expect(format).To(Equal("mp3"))
Expect(bitRate).To(Equal(160)) // Default Bit Rate
})
It("returns requested bitrate", func() {
mf.Suffix = "flac"
mf.BitRate = 1000
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "", 80)
Expect(format).To(Equal("oga"))
Expect(bitRate).To(Equal(80))
})
})
})
})
type fakeFFmpeg struct {
Data string
r io.Reader
closed bool
}
func (ff *fakeFFmpeg) Start(ctx context.Context, cmd, path string, maxBitRate int) (f io.ReadCloser, err error) {
ff.r = strings.NewReader(ff.Data)
return ff, nil
}
func (ff *fakeFFmpeg) Read(p []byte) (n int, err error) {
return ff.r.Read(p)
}
func (ff *fakeFFmpeg) Close() error {
ff.closed = true
return nil
}

View File

@@ -0,0 +1,22 @@
package core
import "github.com/deluan/navidrome/model"
type mockTranscodingRepository struct {
model.TranscodingRepository
}
func (m *mockTranscodingRepository) Get(id string) (*model.Transcoding, error) {
return &model.Transcoding{ID: id, TargetFormat: "mp3", DefaultBitRate: 160}, nil
}
func (m *mockTranscodingRepository) FindByFormat(format string) (*model.Transcoding, error) {
switch format {
case "mp3":
return &model.Transcoding{ID: "mp31", TargetFormat: "mp3", DefaultBitRate: 160}, nil
case "oga":
return &model.Transcoding{ID: "oga1", TargetFormat: "oga", DefaultBitRate: 128}, nil
default:
return nil, model.ErrNotFound
}
}

57
core/transcoder/ffmpeg.go Normal file
View File

@@ -0,0 +1,57 @@
package transcoder
import (
"context"
"io"
"os"
"os/exec"
"strconv"
"strings"
"github.com/deluan/navidrome/log"
)
type Transcoder interface {
Start(ctx context.Context, command, path string, maxBitRate int) (f io.ReadCloser, err error)
}
func New() Transcoder {
path, err := exec.LookPath("ffmpeg")
if err != nil {
log.Error("Unable to find ffmpeg", err)
}
log.Debug("Found ffmpeg", "path", path)
return &ffmpeg{}
}
type ffmpeg struct{}
func (ff *ffmpeg) Start(ctx context.Context, command, path string, maxBitRate int) (f io.ReadCloser, err error) {
args := createTranscodeCommand(command, path, maxBitRate)
log.Trace(ctx, "Executing ffmpeg command", "cmd", args)
cmd := exec.Command(args[0], args[1:]...) // #nosec
cmd.Stderr = os.Stderr
if f, err = cmd.StdoutPipe(); err != nil {
return
}
if err = cmd.Start(); err != nil {
return
}
go func() { _ = cmd.Wait() }() // prevent zombies
return
}
// Path will always be an absolute path
func createTranscodeCommand(cmd, path string, maxBitRate int) []string {
split := strings.Split(cmd, " ")
for i, s := range split {
s = strings.Replace(s, "%s", path, -1)
s = strings.Replace(s, "%b", strconv.Itoa(maxBitRate), -1)
split[i] = s
}
return split
}

View File

@@ -0,0 +1,24 @@
package transcoder
import (
"testing"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/tests"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestTranscoder(t *testing.T) {
tests.Init(t, false)
log.SetLevel(log.LevelCritical)
RegisterFailHandler(Fail)
RunSpecs(t, "Transcoder Suite")
}
var _ = Describe("createTranscodeCommand", func() {
It("creates a valid command line", func() {
args := createTranscodeCommand("ffmpeg -i %s -b:a %bk mp3 -", "/music library/file.mp3", 123)
Expect(args).To(Equal([]string{"ffmpeg", "-i", "/music library/file.mp3", "-b:a", "123k", "mp3", "-"}))
})
})

14
core/wire_providers.go Normal file
View File

@@ -0,0 +1,14 @@
package core
import (
"github.com/deluan/navidrome/core/transcoder"
"github.com/google/wire"
)
var Set = wire.NewSet(
NewArtwork,
NewMediaStreamer,
NewTranscodingCache,
NewImageCache,
transcoder.New,
)

View File

@@ -6,39 +6,43 @@ import (
"sync"
"github.com/deluan/navidrome/conf"
_ "github.com/deluan/navidrome/db/migration"
_ "github.com/deluan/navidrome/db/migrations"
"github.com/deluan/navidrome/log"
_ "github.com/mattn/go-sqlite3"
"github.com/pressly/goose"
)
var (
once sync.Once
Driver = "sqlite3"
Path string
)
func Init() {
var (
once sync.Once
db *sql.DB
)
func Db() *sql.DB {
once.Do(func() {
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)
db, err = sql.Open(Driver, Path)
if err != nil {
panic(err)
}
})
return db
}
func EnsureLatestVersion() {
Init()
db, err := sql.Open(Driver, Path)
defer db.Close()
if err != nil {
log.Error("Failed to open DB", err)
os.Exit(1)
}
db := Db()
err = goose.SetDialect(Driver)
err := goose.SetDialect(Driver)
if err != nil {
log.Error("Invalid DB driver", "driver", Driver, err)
os.Exit(1)

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"
@@ -37,7 +37,7 @@ update annotation set item_type = 'media_file' where item_type = 'mediaFile';
}
func Down20200131183653(tx *sql.Tx) error {
tx.Exec(`
_, err := tx.Exec(`
create table search_dg_tmp
(
id varchar(255) not null
@@ -59,5 +59,5 @@ create index search_table
update annotation set item_type = 'mediaFile' where item_type = 'media_file';
`)
return nil
return err
}

View File

@@ -1,4 +1,4 @@
package migration
package migrations
import (
"database/sql"
@@ -51,6 +51,5 @@ create index annotation_starred
}
func Down20200208222418(tx *sql.Tx) error {
// This code is executed when the migration is rolled back.
return nil
}

View File

@@ -0,0 +1,129 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20200220143731, Down20200220143731)
}
func Up20200220143731(tx *sql.Tx) error {
notice(tx, "This migration will force the next scan to be a full rescan!")
_, err := tx.Exec(`
create table media_file_dg_tmp
(
id varchar(255) not null
primary key,
path varchar(255) default '' not null,
title varchar(255) default '' not null,
album varchar(255) default '' not null,
artist varchar(255) default '' not null,
artist_id varchar(255) default '' not null,
album_artist varchar(255) default '' not null,
album_id varchar(255) default '' not null,
has_cover_art bool default FALSE not null,
track_number integer default 0 not null,
disc_number integer default 0 not null,
year integer default 0 not null,
size integer default 0 not null,
suffix varchar(255) default '' not null,
duration real default 0 not null,
bit_rate integer default 0 not null,
genre varchar(255) default '' not null,
compilation bool default FALSE not null,
created_at datetime,
updated_at datetime
);
insert into media_file_dg_tmp(id, path, title, album, artist, artist_id, album_artist, album_id, has_cover_art, track_number, disc_number, year, size, suffix, duration, bit_rate, genre, compilation, created_at, updated_at) select id, path, title, album, artist, artist_id, album_artist, album_id, has_cover_art, track_number, disc_number, year, size, suffix, duration, bit_rate, genre, compilation, created_at, updated_at from media_file;
drop table media_file;
alter table media_file_dg_tmp rename to media_file;
create index media_file_album_id
on media_file (album_id);
create index media_file_genre
on media_file (genre);
create index media_file_path
on media_file (path);
create index media_file_title
on media_file (title);
create table album_dg_tmp
(
id varchar(255) not null
primary key,
name varchar(255) default '' not null,
artist_id varchar(255) default '' not null,
cover_art_path varchar(255) default '' not null,
cover_art_id varchar(255) default '' not null,
artist varchar(255) default '' not null,
album_artist varchar(255) default '' not null,
year integer default 0 not null,
compilation bool default FALSE not null,
song_count integer default 0 not null,
duration real default 0 not null,
genre varchar(255) default '' not null,
created_at datetime,
updated_at datetime
);
insert into album_dg_tmp(id, name, artist_id, cover_art_path, cover_art_id, artist, album_artist, year, compilation, song_count, duration, genre, created_at, updated_at) select id, name, artist_id, cover_art_path, cover_art_id, artist, album_artist, year, compilation, song_count, duration, genre, created_at, updated_at from album;
drop table album;
alter table album_dg_tmp rename to album;
create index album_artist
on album (artist);
create index album_artist_id
on album (artist_id);
create index album_genre
on album (genre);
create index album_name
on album (name);
create index album_year
on album (year);
create table playlist_dg_tmp
(
id varchar(255) not null
primary key,
name varchar(255) default '' not null,
comment varchar(255) default '' not null,
duration real default 0 not null,
owner varchar(255) default '' not null,
public bool default FALSE not null,
tracks text not null
);
insert into playlist_dg_tmp(id, name, comment, duration, owner, public, tracks) select id, name, comment, duration, owner, public, tracks from playlist;
drop table playlist;
alter table playlist_dg_tmp rename to playlist;
create index playlist_name
on playlist (name);
-- Force a full rescan
delete from property where id like 'LastScan%';
update media_file set updated_at = '0001-01-01';
`)
return err
}
func Down20200220143731(tx *sql.Tx) error {
return nil
}

View File

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

View File

@@ -0,0 +1,53 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20200310181627, Down20200310181627)
}
func Up20200310181627(tx *sql.Tx) error {
_, err := tx.Exec(`
create table transcoding
(
id varchar(255) not null primary key,
name varchar(255) not null,
target_format varchar(255) not null,
command varchar(255) default '' not null,
default_bit_rate int default 192,
unique (name),
unique (target_format)
);
create table player
(
id varchar(255) not null primary key,
name varchar not null,
type varchar,
user_name varchar not null,
client varchar not null,
ip_address varchar,
last_seen timestamp,
max_bit_rate int default 0,
transcoding_id varchar,
unique (name),
foreign key (transcoding_id)
references transcoding(id)
on update restrict
on delete restrict
);
`)
return err
}
func Down20200310181627(tx *sql.Tx) error {
_, err := tx.Exec(`
drop table transcoding;
drop table player;
`)
return err
}

View File

@@ -0,0 +1,41 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20200319211049, Down20200319211049)
}
func Up20200319211049(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table media_file
add full_text varchar(255) default '';
create index if not exists media_file_full_text
on media_file (full_text);
alter table album
add full_text varchar(255) default '';
create index if not exists album_full_text
on album (full_text);
alter table artist
add full_text varchar(255) default '';
create index if not exists artist_full_text
on artist (full_text);
drop table if exists search;
`)
if err != nil {
return err
}
notice(tx, "A full rescan will be performed!")
return forceFullRescan(tx)
}
func Down20200319211049(tx *sql.Tx) error {
return nil
}

View File

@@ -0,0 +1,34 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20200325185135, Down20200325185135)
}
func Up20200325185135(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table album
add album_artist_id varchar(255) default '';
create index album_artist_album_id
on album (album_artist_id);
alter table media_file
add album_artist_id varchar(255) default '';
create index media_file_artist_album_id
on media_file (album_artist_id);
`)
if err != nil {
return err
}
notice(tx, "A full rescan will be performed!")
return forceFullRescan(tx)
}
func Down20200325185135(tx *sql.Tx) error {
return nil
}

View File

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

View File

@@ -0,0 +1,80 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20200327193744, Down20200327193744)
}
func Up20200327193744(tx *sql.Tx) error {
_, err := tx.Exec(`
create table album_dg_tmp
(
id varchar(255) not null
primary key,
name varchar(255) default '' not null,
artist_id varchar(255) default '' not null,
cover_art_path varchar(255) default '' not null,
cover_art_id varchar(255) default '' not null,
artist varchar(255) default '' not null,
album_artist varchar(255) default '' not null,
min_year int default 0 not null,
max_year integer default 0 not null,
compilation bool default FALSE not null,
song_count integer default 0 not null,
duration real default 0 not null,
genre varchar(255) default '' not null,
created_at datetime,
updated_at datetime,
full_text varchar(255) default '',
album_artist_id varchar(255) default ''
);
insert into album_dg_tmp(id, name, artist_id, cover_art_path, cover_art_id, artist, album_artist, max_year, compilation, song_count, duration, genre, created_at, updated_at, full_text, album_artist_id) select id, name, artist_id, cover_art_path, cover_art_id, artist, album_artist, year, compilation, song_count, duration, genre, created_at, updated_at, full_text, album_artist_id from album;
drop table album;
alter table album_dg_tmp rename to album;
create index album_artist
on album (artist);
create index album_artist_album
on album (artist);
create index album_artist_album_id
on album (album_artist_id);
create index album_artist_id
on album (artist_id);
create index album_full_text
on album (full_text);
create index album_genre
on album (genre);
create index album_name
on album (name);
create index album_min_year
on album (min_year);
create index album_max_year
on album (max_year);
`)
if err != nil {
return err
}
notice(tx, "A full rescan will be performed!")
return forceFullRescan(tx)
}
func Down20200327193744(tx *sql.Tx) error {
return nil
}

View File

@@ -0,0 +1,29 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20200404214704, Down20200404214704)
}
func Up20200404214704(tx *sql.Tx) error {
_, err := tx.Exec(`
create index if not exists media_file_year
on media_file (year);
create index if not exists media_file_duration
on media_file (duration);
create index if not exists media_file_track_number
on media_file (disc_number, track_number);
`)
return err
}
func Down20200404214704(tx *sql.Tx) error {
return nil
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,65 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20200423204116, Down20200423204116)
}
func Up20200423204116(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table artist
add order_artist_name varchar(255) collate nocase;
alter table artist
add sort_artist_name varchar(255) collate nocase;
create index if not exists artist_order_artist_name
on artist (order_artist_name);
alter table album
add order_album_name varchar(255) collate nocase;
alter table album
add order_album_artist_name varchar(255) collate nocase;
alter table album
add sort_album_name varchar(255) collate nocase;
alter table album
add sort_artist_name varchar(255) collate nocase;
alter table album
add sort_album_artist_name varchar(255) collate nocase;
create index if not exists album_order_album_name
on album (order_album_name);
create index if not exists album_order_album_artist_name
on album (order_album_artist_name);
alter table media_file
add order_album_name varchar(255) collate nocase;
alter table media_file
add order_album_artist_name varchar(255) collate nocase;
alter table media_file
add order_artist_name varchar(255) collate nocase;
alter table media_file
add sort_album_name varchar(255) collate nocase;
alter table media_file
add sort_artist_name varchar(255) collate nocase;
alter table media_file
add sort_album_artist_name varchar(255) collate nocase;
alter table media_file
add sort_title varchar(255) collate nocase;
create index if not exists media_file_order_album_name
on media_file (order_album_name);
create index if not exists media_file_order_artist_name
on media_file (order_artist_name);
`)
if err != nil {
return err
}
notice(tx, "A full rescan will be performed to change the search behaviour")
return forceFullRescan(tx)
}
func Down20200423204116(tx *sql.Tx) error {
return nil
}

View File

@@ -0,0 +1,27 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20200508093059, Down20200508093059)
}
func Up20200508093059(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table artist
add song_count integer default 0 not null;
`)
if err != nil {
return err
}
notice(tx, "A full rescan will be performed to calculate artists' song counts")
return forceFullRescan(tx)
}
func Down20200508093059(tx *sql.Tx) error {
return nil
}

View File

@@ -0,0 +1,27 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20200512104202, Down20200512104202)
}
func Up20200512104202(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table media_file
add disc_subtitle varchar(255);
`)
if err != nil {
return err
}
notice(tx, "A full rescan will be performed to import disc subtitles")
return forceFullRescan(tx)
}
func Down20200512104202(tx *sql.Tx) error {
return nil
}

View File

@@ -0,0 +1,100 @@
package migrations
import (
"database/sql"
"strings"
"github.com/deluan/navidrome/log"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20200516140647, Down20200516140647)
}
func Up20200516140647(tx *sql.Tx) error {
_, err := tx.Exec(`
create table if not exists playlist_tracks
(
id integer default 0 not null,
playlist_id varchar(255) not null,
media_file_id varchar(255) not null
);
create unique index if not exists playlist_tracks_pos
on playlist_tracks (playlist_id, id);
`)
if err != nil {
return err
}
rows, err := tx.Query("select id, tracks from playlist")
if err != nil {
return err
}
defer rows.Close()
var id, tracks string
for rows.Next() {
err := rows.Scan(&id, &tracks)
if err != nil {
return err
}
err = Up20200516140647UpdatePlaylistTracks(tx, id, tracks)
if err != nil {
return err
}
}
err = rows.Err()
if err != nil {
return err
}
_, err = tx.Exec(`
create table playlist_dg_tmp
(
id varchar(255) not null
primary key,
name varchar(255) default '' not null,
comment varchar(255) default '' not null,
duration real default 0 not null,
song_count integer default 0 not null,
owner varchar(255) default '' not null,
public bool default FALSE not null,
created_at datetime,
updated_at datetime
);
insert into playlist_dg_tmp(id, name, comment, duration, owner, public, created_at, updated_at)
select id, name, comment, duration, owner, public, created_at, updated_at from playlist;
drop table playlist;
alter table playlist_dg_tmp rename to playlist;
create index playlist_name
on playlist (name);
update playlist set song_count = (select count(*) from playlist_tracks where playlist_id = playlist.id)
where id <> ''
`)
return err
}
func Up20200516140647UpdatePlaylistTracks(tx *sql.Tx, id string, tracks string) error {
trackList := strings.Split(tracks, ",")
stmt, err := tx.Prepare("insert into playlist_tracks (playlist_id, media_file_id, id) values (?, ?, ?)")
if err != nil {
return err
}
for i, trackId := range trackList {
_, err := stmt.Exec(id, trackId, i+1)
if err != nil {
log.Error("Error adding track to playlist", "playlistId", id, "trackId", trackId, err)
}
}
return nil
}
func Down20200516140647(tx *sql.Tx) error {
return nil
}

View File

@@ -0,0 +1,137 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20200608153717, Down20200608153717)
}
func Up20200608153717(tx *sql.Tx) error {
// First delete dangling players
_, err := tx.Exec(`
delete from player where user_name not in (select user_name from user)`)
if err != nil {
return err
}
// Also delete dangling players
_, err = tx.Exec(`
delete from playlist where owner not in (select user_name from user)`)
if err != nil {
return err
}
// Also delete dangling playlist tracks
_, err = tx.Exec(`
delete from playlist_tracks where playlist_id not in (select id from playlist)`)
if err != nil {
return err
}
// Add foreign key to player table
err = updatePlayer_20200608153717(tx)
if err != nil {
return err
}
// Add foreign key to playlist table
err = updatePlaylist_20200608153717(tx)
if err != nil {
return err
}
// Add foreign keys to playlist_tracks table
return updatePlaylistTracks_20200608153717(tx)
}
func updatePlayer_20200608153717(tx *sql.Tx) error {
_, err := tx.Exec(`
create table player_dg_tmp
(
id varchar(255) not null
primary key,
name varchar not null
unique,
type varchar,
user_name varchar not null
references user (user_name)
on update cascade on delete cascade,
client varchar not null,
ip_address varchar,
last_seen timestamp,
max_bit_rate int default 0,
transcoding_id varchar null
);
insert into player_dg_tmp(id, name, type, user_name, client, ip_address, last_seen, max_bit_rate, transcoding_id) select id, name, type, user_name, client, ip_address, last_seen, max_bit_rate, transcoding_id from player;
drop table player;
alter table player_dg_tmp rename to player;
`)
return err
}
func updatePlaylist_20200608153717(tx *sql.Tx) error {
_, err := tx.Exec(`
create table playlist_dg_tmp
(
id varchar(255) not null
primary key,
name varchar(255) default '' not null,
comment varchar(255) default '' not null,
duration real default 0 not null,
song_count integer default 0 not null,
owner varchar(255) default '' not null
constraint playlist_user_user_name_fk
references user (user_name)
on update cascade on delete cascade,
public bool default FALSE not null,
created_at datetime,
updated_at datetime
);
insert into playlist_dg_tmp(id, name, comment, duration, song_count, owner, public, created_at, updated_at) select id, name, comment, duration, song_count, owner, public, created_at, updated_at from playlist;
drop table playlist;
alter table playlist_dg_tmp rename to playlist;
create index playlist_name
on playlist (name);
`)
return err
}
func updatePlaylistTracks_20200608153717(tx *sql.Tx) error {
_, err := tx.Exec(`
create table playlist_tracks_dg_tmp
(
id integer default 0 not null,
playlist_id varchar(255) not null
constraint playlist_tracks_playlist_id_fk
references playlist
on update cascade on delete cascade,
media_file_id varchar(255) not null
);
insert into playlist_tracks_dg_tmp(id, playlist_id, media_file_id) select id, playlist_id, media_file_id from playlist_tracks;
drop table playlist_tracks;
alter table playlist_tracks_dg_tmp rename to playlist_tracks;
create unique index playlist_tracks_pos
on playlist_tracks (playlist_id, id);
`)
return err
}
func Down20200608153717(tx *sql.Tx) error {
return nil
}

View File

@@ -0,0 +1,43 @@
package migrations
import (
"database/sql"
"github.com/deluan/navidrome/consts"
"github.com/google/uuid"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(upAddDefaultTranscodings, downAddDefaultTranscodings)
}
func upAddDefaultTranscodings(tx *sql.Tx) error {
row := tx.QueryRow("SELECT COUNT(*) FROM transcoding")
var count int
err := row.Scan(&count)
if err != nil {
return err
}
if count > 0 {
return nil
}
stmt, err := tx.Prepare("insert into transcoding (id, name, target_format, default_bit_rate, command) values (?, ?, ?, ?, ?)")
if err != nil {
return err
}
for _, t := range consts.DefaultTranscodings {
r, _ := uuid.NewRandom()
_, err := stmt.Exec(r.String(), t["name"], t["targetFormat"], t["defaultBitRate"], t["command"])
if err != nil {
return err
}
}
return nil
}
func downAddDefaultTranscodings(tx *sql.Tx) error {
return nil
}

View File

@@ -0,0 +1,27 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(upAddPlaylistPath, downAddPlaylistPath)
}
func upAddPlaylistPath(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table playlist
add path string default '' not null;
alter table playlist
add sync bool default false not null;
`)
return err
}
func downAddPlaylistPath(tx *sql.Tx) error {
return nil
}

View File

@@ -0,0 +1,36 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(upCreatePlayQueuesTable, downCreatePlayQueuesTable)
}
func upCreatePlayQueuesTable(tx *sql.Tx) error {
_, err := tx.Exec(`
create table playqueue
(
id varchar(255) not null primary key,
user_id varchar(255) not null
references user (id)
on update cascade on delete cascade,
comment varchar(255),
current varchar(255) not null,
position integer,
changed_by varchar(255),
items varchar(255),
created_at datetime,
updated_at datetime
);
`)
return err
}
func downCreatePlayQueuesTable(tx *sql.Tx) error {
return nil
}

View File

@@ -0,0 +1,53 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(upCreateBookmarkTable, downCreateBookmarkTable)
}
func upCreateBookmarkTable(tx *sql.Tx) error {
_, err := tx.Exec(`
create table bookmark
(
user_id varchar(255) not null
references user
on update cascade on delete cascade,
item_id varchar(255) not null,
item_type varchar(255) not null,
comment varchar(255),
position integer,
changed_by varchar(255),
created_at datetime,
updated_at datetime,
constraint bookmark_pk
unique (user_id, item_id, item_type)
);
create table playqueue_dg_tmp
(
id varchar(255) not null,
user_id varchar(255) not null
references user
on update cascade on delete cascade,
current varchar(255),
position real,
changed_by varchar(255),
items varchar(255),
created_at datetime,
updated_at datetime
);
drop table playqueue;
alter table playqueue_dg_tmp rename to playqueue;
`)
return err
}
func downCreateBookmarkTable(tx *sql.Tx) error {
return nil
}

View File

@@ -0,0 +1,55 @@
package migrations
import (
"database/sql"
"fmt"
"sync"
"github.com/deluan/navidrome/consts"
)
// Use this in migrations that need to communicate something important (braking changes, forced reindexes, etc...)
func notice(tx *sql.Tx, msg string) {
if isDBInitialized(tx) {
fmt.Printf(`
*************************************************************************************
NOTICE: %s
*************************************************************************************
`, msg)
}
}
// Call this in migrations that requires a full rescan
func forceFullRescan(tx *sql.Tx) error {
_, err := tx.Exec(`
delete from property where id like 'LastScan%';
update media_file set updated_at = '0001-01-01';
`)
return err
}
var once sync.Once
func isDBInitialized(tx *sql.Tx) (initialized bool) {
once.Do(func() {
rows, err := tx.Query("select count(*) from property where id='" + consts.InitialSetupFlagKey + "'")
checkErr(err)
initialized = checkCount(rows) > 0
})
return initialized
}
func checkCount(rows *sql.Rows) (count int) {
for rows.Next() {
err := rows.Scan(&count)
checkErr(err)
}
return count
}
func checkErr(err error) {
if err != nil {
panic(err)
}
}

View File

@@ -1,18 +0,0 @@
# This is just an example. Customize it to your needs.
version: "3"
services:
navidrome:
image: deluan/navidrome:latest
ports:
- "4533:4533"
environment:
# All options with their default values:
ND_MUSICFOLDER: /music
ND_DATAFOLDER: /data
ND_SCANINTERVAL: 1m
ND_LOGLEVEL: info
ND_PORT: 4533
volumes:
- "./data:/data"
- "./music:/music"

View File

@@ -37,7 +37,7 @@ func (b *browser) MediaFolders(ctx context.Context) (model.MediaFolders, error)
func (b *browser) Indexes(ctx context.Context, mediaFolderId string, ifModifiedSince time.Time) (model.ArtistIndexes, time.Time, error) {
// TODO Proper handling of mediaFolderId param
folder, err := b.ds.MediaFolder(ctx).Get(mediaFolderId)
folder, _ := b.ds.MediaFolder(ctx).Get(mediaFolderId)
l, err := b.ds.Property(ctx).DefaultGet(model.PropLastScan+"-"+folder.Path, "-1")
ms, _ := strconv.ParseInt(l, 10, 64)
@@ -61,7 +61,7 @@ type DirectoryInfo struct {
Entries Entries
Parent string
Starred time.Time
PlayCount int32
PlayCount int64
UserRating int
AlbumCount int
CoverArt string
@@ -80,10 +80,6 @@ func (b *browser) Artist(ctx context.Context, id string) (*DirectoryInfo, error)
return nil, err
}
log.Debug(ctx, "Found Artist", "id", id, "name", a.Name)
var albumIds []string
for _, al := range albums {
albumIds = append(albumIds, al.ID)
}
return b.buildArtistDir(a, albums), nil
}
@@ -93,11 +89,6 @@ func (b *browser) Album(ctx context.Context, id string) (*DirectoryInfo, error)
return nil, err
}
log.Debug(ctx, "Found Album", "id", id, "name", al.Name)
var mfIds []string
for _, mf := range tracks {
mfIds = append(mfIds, mf.ID)
}
return b.buildAlbumDir(al, tracks), nil
}
@@ -144,9 +135,10 @@ func (b *browser) buildArtistDir(a *model.Artist, albums model.Albums) *Director
}
dir.Entries = make(Entries, len(albums))
for i, al := range albums {
for i := range albums {
al := albums[i]
dir.Entries[i] = FromAlbum(&al)
dir.PlayCount += int32(al.PlayCount)
dir.PlayCount += al.PlayCount
}
return dir
}
@@ -155,20 +147,23 @@ func (b *browser) buildAlbumDir(al *model.Album, tracks model.MediaFiles) *Direc
dir := &DirectoryInfo{
Id: al.ID,
Name: al.Name,
Parent: al.ArtistID,
Artist: al.Artist,
ArtistId: al.ArtistID,
Parent: al.AlbumArtistID,
Artist: al.AlbumArtist,
ArtistId: al.AlbumArtistID,
SongCount: al.SongCount,
Duration: al.Duration,
Duration: int(al.Duration),
Created: al.CreatedAt,
Year: al.Year,
Year: al.MaxYear,
Genre: al.Genre,
CoverArt: al.CoverArtId,
PlayCount: int32(al.PlayCount),
Starred: al.StarredAt,
PlayCount: al.PlayCount,
UserRating: al.Rating,
}
if al.Starred {
dir.Starred = al.StarredAt
}
dir.Entries = FromMediaFiles(tracks)
return dir
}

View File

@@ -14,7 +14,7 @@ var _ = Describe("Browser", func() {
var repo *mockGenreRepository
var b Browser
BeforeSuite(func() {
BeforeEach(func() {
repo = &mockGenreRepository{data: model.Genres{
{Name: "Rock", SongCount: 1000, AlbumCount: 100},
{Name: "", SongCount: 13, AlbumCount: 13},

View File

@@ -1,46 +1,45 @@
package engine
import (
"fmt"
"context"
"time"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/model/request"
)
type Entry struct {
Id string
Title string
IsDir bool
Parent string
Album string
Year int
Artist string
Genre string
CoverArt string
Starred time.Time
Track int
Duration int
Size int
Suffix string
BitRate int
ContentType string
Path string
PlayCount int32
DiscNumber int
Created time.Time
AlbumId string
ArtistId string
Type string
UserRating int
SongCount int
UserName string
MinutesAgo int
PlayerId int
PlayerName string
AlbumCount int
AbsolutePath string
Id string
Title string
IsDir bool
Parent string
Album string
Year int
Artist string
Genre string
CoverArt string
Starred time.Time
Track int
Duration int
Size int64
Suffix string
BitRate int
ContentType string
Path string
PlayCount int32
DiscNumber int
Created time.Time
AlbumId string
ArtistId string
Type string
UserRating int
SongCount int
UserName string
MinutesAgo int
PlayerId int
PlayerName string
AlbumCount int
BookmarkPosition int64
}
type Entries []Entry
@@ -51,7 +50,9 @@ func FromArtist(ar *model.Artist) Entry {
e.Title = ar.Name
e.AlbumCount = ar.AlbumCount
e.IsDir = true
e.Starred = ar.StarredAt
if ar.Starred {
e.Starred = ar.StarredAt
}
return e
}
@@ -60,18 +61,20 @@ func FromAlbum(al *model.Album) Entry {
e.Id = al.ID
e.Title = al.Name
e.IsDir = true
e.Parent = al.ArtistID
e.Parent = al.AlbumArtistID
e.Album = al.Name
e.Year = al.Year
e.Year = al.MaxYear
e.Artist = al.AlbumArtist
e.Genre = al.Genre
e.CoverArt = al.CoverArtId
e.Created = al.CreatedAt
e.AlbumId = al.ID
e.ArtistId = al.ArtistID
e.Duration = al.Duration
e.ArtistId = al.AlbumArtistID
e.Duration = int(al.Duration)
e.SongCount = al.SongCount
e.Starred = al.StarredAt
if al.Starred {
e.Starred = al.StarredAt
}
e.PlayCount = int32(al.PlayCount)
e.UserRating = al.Rating
return e
@@ -88,44 +91,35 @@ func FromMediaFile(mf *model.MediaFile) Entry {
e.Artist = mf.Artist
e.Genre = mf.Genre
e.Track = mf.TrackNumber
e.Duration = mf.Duration
e.Duration = int(mf.Duration)
e.Size = mf.Size
e.Suffix = mf.Suffix
e.BitRate = mf.BitRate
if mf.HasCoverArt {
e.CoverArt = mf.ID
} else {
e.CoverArt = "al-" + mf.AlbumID
}
e.ContentType = mf.ContentType()
e.AbsolutePath = mf.Path
// Creates a "pseudo" Path, to avoid sending absolute paths to the client
if mf.Path != "" {
e.Path = fmt.Sprintf("%s/%s/%s.%s", realArtistName(mf), mf.Album, mf.Title, mf.Suffix)
}
e.Path = mf.Path
e.DiscNumber = mf.DiscNumber
e.Created = mf.CreatedAt
e.AlbumId = mf.AlbumID
e.ArtistId = mf.ArtistID
e.Type = "music" // TODO Hardcoded for now
e.Type = "music"
e.PlayCount = int32(mf.PlayCount)
e.Starred = mf.StarredAt
e.UserRating = mf.Rating
return e
}
func realArtistName(mf *model.MediaFile) string {
switch {
case mf.Compilation:
return "Various Artists"
case mf.AlbumArtist != "":
return mf.AlbumArtist
if mf.Starred {
e.Starred = mf.StarredAt
}
return mf.Artist
e.UserRating = mf.Rating
e.BookmarkPosition = mf.BookmarkPosition
return e
}
func FromAlbums(albums model.Albums) Entries {
entries := make(Entries, len(albums))
for i, al := range albums {
for i := range albums {
al := albums[i]
entries[i] = FromAlbum(&al)
}
return entries
@@ -133,7 +127,8 @@ func FromAlbums(albums model.Albums) Entries {
func FromMediaFiles(mfs model.MediaFiles) Entries {
entries := make(Entries, len(mfs))
for i, mf := range mfs {
for i := range mfs {
mf := mfs[i]
entries[i] = FromMediaFile(&mf)
}
return entries
@@ -141,8 +136,17 @@ func FromMediaFiles(mfs model.MediaFiles) Entries {
func FromArtists(ars model.Artists) Entries {
entries := make(Entries, len(ars))
for i, ar := range ars {
for i := range ars {
ar := ars[i]
entries[i] = FromArtist(&ar)
}
return entries
}
func userName(ctx context.Context) string {
if user, ok := request.UserFrom(ctx); !ok {
return "UNKNOWN"
} else {
return user.UserName
}
}

View File

@@ -1,112 +0,0 @@
package engine
import (
"bytes"
"context"
"errors"
"image"
_ "image/gif"
"image/jpeg"
_ "image/png"
"io"
"net/http"
"os"
"strings"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/static"
"github.com/dhowden/tag"
"github.com/nfnt/resize"
)
type Cover interface {
Get(ctx context.Context, id string, size int, out io.Writer) error
}
type cover struct {
ds model.DataStore
}
func NewCover(ds model.DataStore) Cover {
return &cover{ds}
}
func (c *cover) getCoverPath(ctx context.Context, id string) (string, error) {
switch {
case strings.HasPrefix(id, "al-"):
id = id[3:]
al, err := c.ds.Album(ctx).Get(id)
if err != nil {
return "", err
}
return al.CoverArtPath, nil
default:
mf, err := c.ds.MediaFile(ctx).Get(id)
if err != nil {
return "", err
}
if mf.HasCoverArt {
return mf.Path, nil
}
}
return "", model.ErrNotFound
}
func (c *cover) Get(ctx context.Context, id string, size int, out io.Writer) error {
path, err := c.getCoverPath(ctx, id)
if err != nil && err != model.ErrNotFound {
return err
}
var reader io.Reader
if err != model.ErrNotFound {
reader, err = readFromTag(path)
} else {
var f http.File
f, err = static.AssetFile().Open("default_cover.jpg")
if err == nil {
defer f.Close()
reader = f
}
}
if err != nil {
return model.ErrNotFound
}
if size > 0 {
return resizeImage(reader, size, out)
}
_, err = io.Copy(out, reader)
return err
}
func resizeImage(reader io.Reader, size int, out io.Writer) error {
img, _, err := image.Decode(reader)
if err != nil {
return err
}
m := resize.Resize(uint(size), 0, img, resize.NearestNeighbor)
return jpeg.Encode(out, m, &jpeg.Options{Quality: 75})
}
func readFromTag(path string) (io.Reader, 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("error extracting art from file " + path)
}
return bytes.NewReader(picture.Data), nil
}

View File

@@ -9,88 +9,108 @@ import (
)
type ListGenerator interface {
GetNewest(ctx context.Context, offset int, size int) (Entries, error)
GetRecent(ctx context.Context, offset int, size int) (Entries, error)
GetFrequent(ctx context.Context, offset int, size int) (Entries, error)
GetHighest(ctx context.Context, offset int, size int) (Entries, error)
GetRandom(ctx context.Context, offset int, size int) (Entries, error)
GetByName(ctx context.Context, offset int, size int) (Entries, error)
GetByArtist(ctx context.Context, offset int, size int) (Entries, error)
GetStarred(ctx context.Context, offset int, size int) (Entries, error)
GetAllStarred(ctx context.Context) (artists Entries, albums Entries, mediaFiles Entries, err error)
GetNowPlaying(ctx context.Context) (Entries, error)
GetRandomSongs(ctx context.Context, size int, genre string) (Entries, error)
GetSongs(ctx context.Context, offset, size int, filter ListFilter) (Entries, error)
GetAlbums(ctx context.Context, offset, size int, filter ListFilter) (Entries, error)
}
func NewListGenerator(ds model.DataStore, npRepo NowPlayingRepository) ListGenerator {
return &listGenerator{ds, npRepo}
}
type ListFilter model.QueryOptions
func ByNewest() ListFilter {
return ListFilter{Sort: "createdAt", Order: "desc"}
}
func ByRecent() ListFilter {
return ListFilter{Sort: "playDate", Order: "desc", Filters: squirrel.Gt{"play_date": time.Time{}}}
}
func ByFrequent() ListFilter {
return ListFilter{Sort: "playCount", Order: "desc", Filters: squirrel.Gt{"play_count": 0}}
}
func ByRandom() ListFilter {
return ListFilter{Sort: "random()"}
}
func ByName() ListFilter {
return ListFilter{Sort: "name"}
}
func ByArtist() ListFilter {
return ListFilter{Sort: "artist"}
}
func ByStarred() ListFilter {
return ListFilter{Sort: "starred_at", Order: "desc", Filters: squirrel.Eq{"starred": true}}
}
func ByRating() ListFilter {
return ListFilter{Sort: "Rating", Order: "desc", Filters: squirrel.Gt{"rating": 0}}
}
func ByGenre(genre string) ListFilter {
return ListFilter{
Sort: "genre asc, name asc",
Filters: squirrel.Eq{"genre": genre},
}
}
func ByYear(fromYear, toYear int) ListFilter {
return ListFilter{
Sort: "max_year, name",
Filters: squirrel.Or{
squirrel.And{
squirrel.GtOrEq{"min_year": fromYear},
squirrel.LtOrEq{"min_year": toYear},
},
squirrel.And{
squirrel.GtOrEq{"max_year": fromYear},
squirrel.LtOrEq{"max_year": toYear},
},
},
}
}
func SongsByGenre(genre string) ListFilter {
return ListFilter{
Sort: "genre asc, title asc",
Filters: squirrel.Eq{"genre": genre},
}
}
func SongsByRandom(genre string, fromYear, toYear int) ListFilter {
options := ListFilter{
Sort: "random()",
}
ff := squirrel.And{}
if genre != "" {
ff = append(ff, squirrel.Eq{"genre": genre})
}
if fromYear != 0 {
ff = append(ff, squirrel.GtOrEq{"year": fromYear})
}
if toYear != 0 {
ff = append(ff, squirrel.LtOrEq{"year": toYear})
}
options.Filters = ff
return options
}
type listGenerator struct {
ds model.DataStore
npRepo NowPlayingRepository
}
func (g *listGenerator) query(ctx context.Context, qo model.QueryOptions) (Entries, error) {
albums, err := g.ds.Album(ctx).GetAll(qo)
if err != nil {
return nil, err
}
albumIds := make([]string, len(albums))
for i, al := range albums {
albumIds[i] = al.ID
}
return FromAlbums(albums), err
}
func (g *listGenerator) GetNewest(ctx context.Context, offset int, size int) (Entries, error) {
qo := model.QueryOptions{Sort: "CreatedAt", Order: "desc", Offset: offset, Max: size}
return g.query(ctx, qo)
}
func (g *listGenerator) GetRecent(ctx context.Context, offset int, size int) (Entries, error) {
qo := model.QueryOptions{Sort: "PlayDate", Order: "desc", Offset: offset, Max: size,
Filters: squirrel.Gt{"play_date": time.Time{}}}
return g.query(ctx, qo)
}
func (g *listGenerator) GetFrequent(ctx context.Context, offset int, size int) (Entries, error) {
qo := model.QueryOptions{Sort: "PlayCount", Order: "desc", Offset: offset, Max: size,
Filters: squirrel.Gt{"play_count": 0}}
return g.query(ctx, qo)
}
func (g *listGenerator) GetHighest(ctx context.Context, offset int, size int) (Entries, error) {
qo := model.QueryOptions{Sort: "Rating", Order: "desc", Offset: offset, Max: size,
Filters: squirrel.Gt{"rating": 0}}
return g.query(ctx, qo)
}
func (g *listGenerator) GetByName(ctx context.Context, offset int, size int) (Entries, error) {
qo := model.QueryOptions{Sort: "Name", Offset: offset, Max: size}
return g.query(ctx, qo)
}
func (g *listGenerator) GetByArtist(ctx context.Context, offset int, size int) (Entries, error) {
qo := model.QueryOptions{Sort: "Artist", Offset: offset, Max: size}
return g.query(ctx, qo)
}
func (g *listGenerator) GetRandom(ctx context.Context, offset int, size int) (Entries, error) {
albums, err := g.ds.Album(ctx).GetRandom(model.QueryOptions{Max: size, Offset: offset})
if err != nil {
return nil, err
}
return FromAlbums(albums), nil
}
func (g *listGenerator) GetRandomSongs(ctx context.Context, size int, genre string) (Entries, error) {
options := model.QueryOptions{Max: size}
if genre != "" {
options.Filters = squirrel.Eq{"genre": genre}
}
mediaFiles, err := g.ds.MediaFile(ctx).GetRandom(options)
func (g *listGenerator) GetSongs(ctx context.Context, offset, size int, filter ListFilter) (Entries, error) {
qo := model.QueryOptions(filter)
qo.Offset = offset
qo.Max = size
mediaFiles, err := g.ds.MediaFile(ctx).GetAll(qo)
if err != nil {
return nil, err
}
@@ -98,6 +118,18 @@ func (g *listGenerator) GetRandomSongs(ctx context.Context, size int, genre stri
return FromMediaFiles(mediaFiles), nil
}
func (g *listGenerator) GetAlbums(ctx context.Context, offset, size int, filter ListFilter) (Entries, error) {
qo := model.QueryOptions(filter)
qo.Offset = offset
qo.Max = size
albums, err := g.ds.Album(ctx).GetAll(qo)
if err != nil {
return nil, err
}
return FromAlbums(albums), nil
}
func (g *listGenerator) GetStarred(ctx context.Context, offset int, size int) (Entries, error) {
qo := model.QueryOptions{Offset: offset, Max: size, Sort: "starred_at", Order: "desc"}
albums, err := g.ds.Album(ctx).GetStarred(qo)
@@ -126,16 +158,6 @@ func (g *listGenerator) GetAllStarred(ctx context.Context) (artists Entries, alb
return nil, nil, nil, err
}
var mfIds []string
for _, mf := range mfs {
mfIds = append(mfIds, mf.ID)
}
var artistIds []string
for _, ar := range ars {
artistIds = append(artistIds, ar.ID)
}
artists = FromArtists(ars)
albums = FromAlbums(als)
mediaFiles = FromMediaFiles(mfs)
@@ -156,10 +178,9 @@ func (g *listGenerator) GetNowPlaying(ctx context.Context) (Entries, error) {
}
entries[i] = FromMediaFile(mf)
entries[i].UserName = np.Username
entries[i].MinutesAgo = int(time.Now().Sub(np.Start).Minutes())
entries[i].MinutesAgo = int(time.Since(np.Start).Minutes())
entries[i].PlayerId = np.PlayerId
entries[i].PlayerName = np.PlayerName
}
return entries, nil
}

View File

@@ -1,233 +0,0 @@
package engine
import (
"context"
"io"
"io/ioutil"
"mime"
"os"
"os/exec"
"strconv"
"strings"
"time"
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/utils"
)
type MediaStreamer interface {
NewStream(ctx context.Context, id string, maxBitRate int, format string) (mediaStream, error)
}
func NewMediaStreamer(ds model.DataStore) MediaStreamer {
return &mediaStreamer{ds: ds}
}
type mediaStream interface {
io.ReadSeeker
ContentType() string
Name() string
ModTime() time.Time
Close() error
Duration() int
}
type mediaStreamer struct {
ds model.DataStore
}
func (ms *mediaStreamer) NewStream(ctx context.Context, id string, maxBitRate int, format string) (mediaStream, error) {
mf, err := ms.ds.MediaFile(ctx).Get(id)
if err != nil {
return nil, err
}
var bitRate int
if format == "raw" || !conf.Server.EnableDownsampling {
bitRate = mf.BitRate
format = mf.Suffix
} else {
if maxBitRate == 0 {
bitRate = mf.BitRate
} else {
bitRate = utils.MinInt(mf.BitRate, maxBitRate)
}
format = mf.Suffix
}
if conf.Server.MaxBitRate != 0 {
bitRate = utils.MinInt(bitRate, conf.Server.MaxBitRate)
}
var stream mediaStream
if bitRate == mf.BitRate && mime.TypeByExtension("."+format) == mf.ContentType() {
log.Debug(ctx, "Streaming raw file", "id", mf.ID, "path", mf.Path,
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix)
f, err := os.Open(mf.Path)
if err != nil {
return nil, err
}
stream = &rawMediaStream{ctx: ctx, mf: mf, file: f}
return stream, nil
}
log.Debug(ctx, "Streaming transcoded file", "id", mf.ID, "path", mf.Path,
"requestBitrate", bitRate, "requestFormat", format,
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix)
f := &transcodedMediaStream{ctx: ctx, mf: mf, bitRate: bitRate, format: format}
return f, err
}
type rawMediaStream struct {
file *os.File
ctx context.Context
mf *model.MediaFile
}
func (m *rawMediaStream) Read(p []byte) (n int, err error) {
return m.file.Read(p)
}
func (m *rawMediaStream) Seek(offset int64, whence int) (int64, error) {
return m.file.Seek(offset, whence)
}
func (m *rawMediaStream) ContentType() string {
return m.mf.ContentType()
}
func (m *rawMediaStream) Name() string {
return m.mf.Path
}
func (m *rawMediaStream) ModTime() time.Time {
return m.mf.UpdatedAt
}
func (m *rawMediaStream) Duration() int {
return m.mf.Duration
}
func (m *rawMediaStream) Close() error {
log.Trace(m.ctx, "Closing file", "id", m.mf.ID, "path", m.mf.Path)
return m.file.Close()
}
type transcodedMediaStream struct {
ctx context.Context
mf *model.MediaFile
pipe io.ReadCloser
bitRate int
format string
skip int64
pos int64
}
func (m *transcodedMediaStream) Read(p []byte) (n int, err error) {
// Open the pipe and optionally skip a initial chunk of the stream (to simulate a Seek)
if m.pipe == nil {
m.pipe, err = newTranscode(m.ctx, m.mf.Path, m.bitRate, m.format)
if err != nil {
return 0, err
}
if m.skip > 0 {
_, err := io.CopyN(ioutil.Discard, m.pipe, m.skip)
m.pos = m.skip
if err != nil {
return 0, err
}
}
}
n, err = m.pipe.Read(p)
m.pos += int64(n)
if err == io.EOF {
m.Close()
}
return
}
// This is an attempt to make a pipe seekable. It is very wasteful, restarting the stream every time
// a Seek happens. This is ok-ish for audio, but would kill the server for video.
func (m *transcodedMediaStream) Seek(offset int64, whence int) (int64, error) {
size := int64((m.mf.Duration)*m.bitRate*1000) / 8
log.Trace(m.ctx, "Seeking transcoded stream", "path", m.mf.Path, "offset", offset, "whence", whence, "size", size)
switch whence {
case io.SeekEnd:
m.skip = size - offset
offset = size
case io.SeekStart:
m.skip = offset
case io.SeekCurrent:
io.CopyN(ioutil.Discard, m.pipe, offset)
m.pos += offset
offset = m.pos
}
// If need to Seek to a previous position, close the pipe (will be restarted on next Read)
var err error
if whence != io.SeekCurrent {
if m.pipe != nil {
err = m.Close()
}
}
return offset, err
}
func (m *transcodedMediaStream) ContentType() string {
return mime.TypeByExtension(".mp3")
}
func (m *transcodedMediaStream) Name() string {
return m.mf.Path
}
func (m *transcodedMediaStream) ModTime() time.Time {
return m.mf.UpdatedAt
}
func (m *transcodedMediaStream) Duration() int {
return m.mf.Duration
}
func (m *transcodedMediaStream) Close() error {
log.Trace(m.ctx, "Closing stream", "id", m.mf.ID, "path", m.mf.Path)
err := m.pipe.Close()
m.pipe = nil
m.pos = 0
return err
}
func newTranscode(ctx context.Context, path string, maxBitRate int, format string) (f io.ReadCloser, err error) {
cmdLine, args := createTranscodeCommand(path, maxBitRate, format)
log.Trace(ctx, "Executing ffmpeg command", "arg0", cmdLine, "args", args)
cmd := exec.Command(cmdLine, args...)
cmd.Stderr = os.Stderr
if f, err = cmd.StdoutPipe(); err != nil {
return f, err
}
if err = cmd.Start(); err != nil {
return f, err
}
go cmd.Wait() // prevent zombies
return f, err
}
func createTranscodeCommand(path string, maxBitRate int, format string) (string, []string) {
cmd := conf.Server.DownsampleCommand
split := strings.Split(cmd, " ")
for i, s := range split {
s = strings.Replace(s, "%s", path, -1)
s = strings.Replace(s, "%b", strconv.Itoa(maxBitRate), -1)
split[i] = s
}
return split[0], split[1:]
}

View File

@@ -1,75 +0,0 @@
package engine
import (
"time"
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/persistence"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("MediaStreamer", func() {
var streamer MediaStreamer
var ds model.DataStore
ctx := log.NewContext(nil)
BeforeEach(func() {
conf.Server.EnableDownsampling = true
ds = &persistence.MockDataStore{}
ds.MediaFile(ctx).(*persistence.MockMediaFile).SetData(`[{"id": "123", "path": "tests/fixtures/test.mp3", "bitRate": 128}]`, 1)
streamer = NewMediaStreamer(ds)
})
Context("NewStream", func() {
It("returns a rawMediaStream if format is 'raw'", func() {
Expect(streamer.NewStream(ctx, "123", 0, "raw")).To(BeAssignableToTypeOf(&rawMediaStream{}))
})
It("returns a rawMediaStream if maxBitRate is 0", func() {
Expect(streamer.NewStream(ctx, "123", 0, "mp3")).To(BeAssignableToTypeOf(&rawMediaStream{}))
})
It("returns a rawMediaStream if maxBitRate is higher than file bitRate", func() {
Expect(streamer.NewStream(ctx, "123", 256, "mp3")).To(BeAssignableToTypeOf(&rawMediaStream{}))
})
It("returns a transcodedMediaStream if maxBitRate is lower than file bitRate", func() {
s, err := streamer.NewStream(ctx, "123", 64, "mp3")
Expect(err).To(BeNil())
Expect(s).To(BeAssignableToTypeOf(&transcodedMediaStream{}))
Expect(s.(*transcodedMediaStream).bitRate).To(Equal(64))
})
})
Context("rawMediaStream", func() {
var rawStream mediaStream
var modTime time.Time
BeforeEach(func() {
modTime = time.Now()
mf := &model.MediaFile{ID: "123", Path: "test.mp3", UpdatedAt: modTime, Suffix: "mp3"}
rawStream = &rawMediaStream{mf: mf, ctx: ctx}
})
It("returns the ContentType", func() {
Expect(rawStream.ContentType()).To(Equal("audio/mpeg"))
})
It("returns the ModTime", func() {
Expect(rawStream.ModTime()).To(Equal(modTime))
})
})
Context("createTranscodeCommand", func() {
BeforeEach(func() {
conf.Server.DownsampleCommand = "ffmpeg -i %s -b:a %bk mp3 -"
})
It("creates a valid command line", func() {
cmd, args := createTranscodeCommand("/music library/file.mp3", 123, "")
Expect(cmd).To(Equal("ffmpeg"))
Expect(args).To(Equal([]string{"-i", "/music library/file.mp3", "-b:a", "123k", "mp3", "-"}))
})
})
})

View File

@@ -1,86 +0,0 @@
package engine
import (
"errors"
"time"
)
func CreateMockNowPlayingRepo() *MockNowPlaying {
return &MockNowPlaying{}
}
type MockNowPlaying struct {
NowPlayingRepository
data []NowPlayingInfo
t time.Time
err bool
}
func (m *MockNowPlaying) SetError(err bool) {
m.err = err
}
func (m *MockNowPlaying) Enqueue(info *NowPlayingInfo) error {
if m.err {
return errors.New("Error!")
}
m.data = append(m.data, NowPlayingInfo{})
copy(m.data[1:], m.data[0:])
m.data[0] = *info
if !m.t.IsZero() {
m.data[0].Start = m.t
m.t = time.Time{}
}
return nil
}
func (m *MockNowPlaying) Dequeue(playerId int) (*NowPlayingInfo, error) {
if len(m.data) == 0 {
return nil, nil
}
l := len(m.data)
info := m.data[l-1]
m.data = m.data[:l-1]
return &info, nil
}
func (m *MockNowPlaying) Count(playerId int) (int64, error) {
return int64(len(m.data)), nil
}
func (m *MockNowPlaying) GetAll() ([]*NowPlayingInfo, error) {
np, err := m.Head(1)
if np == nil || err != nil {
return nil, err
}
return []*NowPlayingInfo{np}, err
}
func (m *MockNowPlaying) Head(playerId int) (*NowPlayingInfo, error) {
if len(m.data) == 0 {
return nil, nil
}
info := m.data[0]
return &info, nil
}
func (m *MockNowPlaying) Tail(playerId int) (*NowPlayingInfo, error) {
if len(m.data) == 0 {
return nil, nil
}
info := m.data[len(m.data)-1]
return &info, nil
}
func (m *MockNowPlaying) ClearAll() {
m.data = make([]NowPlayingInfo, 0)
m.err = false
}
func (m *MockNowPlaying) OverrideNow(t time.Time) {
m.t = t
}

View File

@@ -1,46 +0,0 @@
package engine
import (
"errors"
"github.com/deluan/navidrome/model"
)
func CreateMockPropertyRepo() *MockProperty {
return &MockProperty{data: make(map[string]string)}
}
type MockProperty struct {
model.PropertyRepository
data map[string]string
err bool
}
func (m *MockProperty) SetError(err bool) {
m.err = err
}
func (m *MockProperty) Put(id string, value string) error {
if m.err {
return errors.New("Error!")
}
m.data[id] = value
return nil
}
func (m *MockProperty) Get(id string) (string, error) {
if m.err {
return "", errors.New("Error!")
}
return m.data[id], nil
}
func (m *MockProperty) DefaultGet(id string, defaultValue string) (string, error) {
v, err := m.Get(id)
if v == "" {
v = defaultValue
}
return v, err
}

View File

@@ -0,0 +1,22 @@
package engine
import "github.com/deluan/navidrome/model"
type mockTranscodingRepository struct {
model.TranscodingRepository
}
func (m *mockTranscodingRepository) Get(id string) (*model.Transcoding, error) {
return &model.Transcoding{ID: id, TargetFormat: "mp3", DefaultBitRate: 160}, nil
}
func (m *mockTranscodingRepository) FindByFormat(format string) (*model.Transcoding, error) {
switch format {
case "mp3":
return &model.Transcoding{ID: "mp31", TargetFormat: "mp3", DefaultBitRate: 160}, nil
case "oga":
return &model.Transcoding{ID: "oga1", TargetFormat: "oga", DefaultBitRate: 128}, nil
default:
return nil, model.ErrNotFound
}
}

View File

@@ -110,7 +110,7 @@ func checkExpired(l *list.List, f func() *list.Element) *list.Element {
return nil
}
start := e.Value.(*NowPlayingInfo).Start
if time.Now().Sub(start) < NowPlayingExpire {
if time.Since(start) < NowPlayingExpire {
return e
}
l.Remove(e)

68
engine/players.go Normal file
View File

@@ -0,0 +1,68 @@
package engine
import (
"context"
"fmt"
"time"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/model/request"
"github.com/google/uuid"
)
type Players interface {
Get(ctx context.Context, playerId string) (*model.Player, error)
Register(ctx context.Context, id, client, typ, ip string) (*model.Player, *model.Transcoding, error)
}
func NewPlayers(ds model.DataStore) Players {
return &players{ds}
}
type players struct {
ds model.DataStore
}
func (p *players) Register(ctx context.Context, id, client, typ, ip string) (*model.Player, *model.Transcoding, error) {
var plr *model.Player
var trc *model.Transcoding
var err error
userName, _ := request.UsernameFrom(ctx)
if id != "" {
plr, err = p.ds.Player(ctx).Get(id)
if err == nil && plr.Client != client {
id = ""
}
}
if err != nil || id == "" {
plr, err = p.ds.Player(ctx).FindByName(client, userName)
if err == nil {
log.Debug("Found player by name", "id", plr.ID, "client", client, "username", userName)
} else {
r, _ := uuid.NewRandom()
plr = &model.Player{
ID: r.String(),
Name: fmt.Sprintf("%s (%s)", client, userName),
UserName: userName,
Client: client,
}
log.Info("Registering new player", "id", plr.ID, "client", client, "username", userName)
}
}
plr.LastSeen = time.Now()
plr.Type = typ
plr.IPAddress = ip
err = p.ds.Player(ctx).Put(plr)
if err != nil {
return nil, nil, err
}
if plr.TranscodingId != "" {
trc, err = p.ds.Transcoding(ctx).Get(plr.TranscodingId)
}
return plr, trc, err
}
func (p *players) Get(ctx context.Context, playerId string) (*model.Player, error) {
return p.ds.Player(ctx).Get(playerId)
}

140
engine/players_test.go Normal file
View File

@@ -0,0 +1,140 @@
package engine
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/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Players", func() {
var players Players
var repo *mockPlayerRepository
ctx := log.NewContext(context.TODO())
ctx = request.WithUser(ctx, model.User{ID: "userid", UserName: "johndoe"})
ctx = request.WithUsername(ctx, "johndoe")
var beforeRegister time.Time
BeforeEach(func() {
repo = &mockPlayerRepository{}
ds := &persistence.MockDataStore{MockedPlayer: repo, MockedTranscoding: &mockTranscodingRepository{}}
players = NewPlayers(ds)
beforeRegister = time.Now()
})
Describe("Register", func() {
It("creates a new player when no ID is specified", func() {
p, trc, err := players.Register(ctx, "", "client", "chrome", "1.2.3.4")
Expect(err).ToNot(HaveOccurred())
Expect(p.ID).ToNot(BeEmpty())
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
Expect(p.Client).To(Equal("client"))
Expect(p.UserName).To(Equal("johndoe"))
Expect(p.Type).To(Equal("chrome"))
Expect(repo.lastSaved).To(Equal(p))
Expect(trc).To(BeNil())
})
It("creates a new player if it cannot find any matching player", func() {
p, trc, err := players.Register(ctx, "123", "client", "chrome", "1.2.3.4")
Expect(err).ToNot(HaveOccurred())
Expect(p.ID).ToNot(BeEmpty())
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
Expect(repo.lastSaved).To(Equal(p))
Expect(trc).To(BeNil())
})
It("creates a new player if client does not match the one in DB", func() {
plr := &model.Player{ID: "123", Name: "A Player", Client: "client1111", LastSeen: time.Time{}}
repo.add(plr)
p, trc, err := players.Register(ctx, "123", "client2222", "chrome", "1.2.3.4")
Expect(err).ToNot(HaveOccurred())
Expect(p.ID).ToNot(BeEmpty())
Expect(p.ID).ToNot(Equal("123"))
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
Expect(p.Client).To(Equal("client2222"))
Expect(trc).To(BeNil())
})
It("finds players by ID", func() {
plr := &model.Player{ID: "123", Name: "A Player", Client: "client", LastSeen: time.Time{}}
repo.add(plr)
p, trc, err := players.Register(ctx, "123", "client", "chrome", "1.2.3.4")
Expect(err).ToNot(HaveOccurred())
Expect(p.ID).To(Equal("123"))
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
Expect(repo.lastSaved).To(Equal(p))
Expect(trc).To(BeNil())
})
It("finds player by client and user names when ID is not found", func() {
plr := &model.Player{ID: "123", Name: "A Player", Client: "client", UserName: "johndoe", LastSeen: time.Time{}}
repo.add(plr)
p, _, err := players.Register(ctx, "999", "client", "chrome", "1.2.3.4")
Expect(err).ToNot(HaveOccurred())
Expect(p.ID).To(Equal("123"))
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
Expect(repo.lastSaved).To(Equal(p))
})
It("finds player by client and user names when not ID is provided", func() {
plr := &model.Player{ID: "123", Name: "A Player", Client: "client", UserName: "johndoe", LastSeen: time.Time{}}
repo.add(plr)
p, _, err := players.Register(ctx, "", "client", "chrome", "1.2.3.4")
Expect(err).ToNot(HaveOccurred())
Expect(p.ID).To(Equal("123"))
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
Expect(repo.lastSaved).To(Equal(p))
})
It("finds player by ID and return its transcoding", func() {
plr := &model.Player{ID: "123", Name: "A Player", Client: "client", LastSeen: time.Time{}, TranscodingId: "1"}
repo.add(plr)
p, trc, err := players.Register(ctx, "123", "client", "chrome", "1.2.3.4")
Expect(err).ToNot(HaveOccurred())
Expect(p.ID).To(Equal("123"))
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
Expect(repo.lastSaved).To(Equal(p))
Expect(trc.ID).To(Equal("1"))
})
})
})
type mockPlayerRepository struct {
model.PlayerRepository
lastSaved *model.Player
data map[string]model.Player
}
func (m *mockPlayerRepository) add(p *model.Player) {
if m.data == nil {
m.data = make(map[string]model.Player)
}
m.data[p.ID] = *p
}
func (m *mockPlayerRepository) Get(id string) (*model.Player, error) {
if p, ok := m.data[id]; ok {
return &p, nil
}
return nil, model.ErrNotFound
}
func (m *mockPlayerRepository) FindByName(client, userName string) (*model.Player, error) {
for _, p := range m.data {
if p.Client == client && p.UserName == userName {
return &p, nil
}
}
return nil, model.ErrNotFound
}
func (m *mockPlayerRepository) Put(p *model.Player) error {
m.lastSaved = p
return nil
}

View File

@@ -2,8 +2,10 @@ package engine
import (
"context"
"time"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/model/request"
"github.com/deluan/navidrome/utils"
)
@@ -24,34 +26,37 @@ type playlists struct {
}
func (p *playlists) Create(ctx context.Context, playlistId, name string, ids []string) error {
owner := p.getUser(ctx)
var pls *model.Playlist
var err error
// If playlistID is present, override tracks
if playlistId != "" {
pls, err = p.ds.Playlist(ctx).Get(playlistId)
if err != nil {
return err
}
if owner != pls.Owner {
return model.ErrNotAuthorized
}
pls.Tracks = nil
} else {
pls = &model.Playlist{
Name: name,
Owner: owner,
}
}
for _, id := range ids {
pls.Tracks = append(pls.Tracks, model.MediaFile{ID: id})
}
return p.ds.WithTx(func(tx model.DataStore) error {
owner := p.getUser(ctx)
var pls *model.Playlist
var err error
return p.ds.Playlist(ctx).Put(pls)
// If playlistID is present, override tracks
if playlistId != "" {
pls, err = tx.Playlist(ctx).Get(playlistId)
if err != nil {
return err
}
if owner != pls.Owner {
return model.ErrNotAuthorized
}
pls.Tracks = nil
} else {
pls = &model.Playlist{
Name: name,
Owner: owner,
}
}
for _, id := range ids {
pls.Tracks = append(pls.Tracks, model.MediaFile{ID: id})
}
return tx.Playlist(ctx).Put(pls)
})
}
func (p *playlists) getUser(ctx context.Context) string {
user, ok := ctx.Value("user").(*model.User)
user, ok := request.UserFrom(ctx)
if ok {
return user.UserName
}
@@ -59,50 +64,54 @@ func (p *playlists) getUser(ctx context.Context) string {
}
func (p *playlists) Delete(ctx context.Context, playlistId string) error {
pls, err := p.ds.Playlist(ctx).Get(playlistId)
if err != nil {
return err
}
return p.ds.WithTx(func(tx model.DataStore) error {
pls, err := tx.Playlist(ctx).Get(playlistId)
if err != nil {
return err
}
owner := p.getUser(ctx)
if owner != pls.Owner {
return model.ErrNotAuthorized
}
return p.ds.Playlist(nil).Delete(playlistId)
owner := p.getUser(ctx)
if owner != pls.Owner {
return model.ErrNotAuthorized
}
return tx.Playlist(ctx).Delete(playlistId)
})
}
func (p *playlists) Update(ctx context.Context, playlistId string, name *string, idsToAdd []string, idxToRemove []int) error {
pls, err := p.ds.Playlist(ctx).Get(playlistId)
owner := p.getUser(ctx)
if owner != pls.Owner {
return model.ErrNotAuthorized
}
if err != nil {
return err
}
if name != nil {
pls.Name = *name
}
newTracks := model.MediaFiles{}
for i, t := range pls.Tracks {
if utils.IntInSlice(i, idxToRemove) {
continue
return p.ds.WithTx(func(tx model.DataStore) error {
pls, err := tx.Playlist(ctx).Get(playlistId)
if err != nil {
return err
}
newTracks = append(newTracks, t)
}
for _, id := range idsToAdd {
newTracks = append(newTracks, model.MediaFile{ID: id})
}
pls.Tracks = newTracks
owner := p.getUser(ctx)
if owner != pls.Owner {
return model.ErrNotAuthorized
}
return p.ds.Playlist(ctx).Put(pls)
if name != nil {
pls.Name = *name
}
newTracks := model.MediaFiles{}
for i, t := range pls.Tracks {
if utils.IntInSlice(i, idxToRemove) {
continue
}
newTracks = append(newTracks, t)
}
for _, id := range idsToAdd {
newTracks = append(newTracks, model.MediaFile{ID: id})
}
pls.Tracks = newTracks
return tx.Playlist(ctx).Put(pls)
})
}
func (p *playlists) GetAll(ctx context.Context) (model.Playlists, error) {
return p.ds.Playlist(ctx).GetAll(model.QueryOptions{})
return p.ds.Playlist(ctx).GetAll()
}
type PlaylistInfo struct {
@@ -114,10 +123,12 @@ type PlaylistInfo struct {
Public bool
Owner string
Comment string
Created time.Time
Changed time.Time
}
func (p *playlists) Get(ctx context.Context, id string) (*PlaylistInfo, error) {
pl, err := p.ds.Playlist(ctx).GetWithTracks(id)
pl, err := p.ds.Playlist(ctx).Get(id)
if err != nil {
return nil, err
}
@@ -126,11 +137,13 @@ func (p *playlists) Get(ctx context.Context, id string) (*PlaylistInfo, error) {
plsInfo := &PlaylistInfo{
Id: pl.ID,
Name: pl.Name,
SongCount: len(pl.Tracks),
Duration: pl.Duration,
SongCount: pl.SongCount,
Duration: int(pl.Duration),
Public: pl.Public,
Owner: pl.Owner,
Comment: pl.Comment,
Changed: pl.UpdatedAt,
Created: pl.CreatedAt,
}
plsInfo.Entries = FromMediaFiles(pl.Tracks)

View File

@@ -2,10 +2,10 @@ package engine
import (
"context"
"errors"
"fmt"
"time"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
)
@@ -42,6 +42,13 @@ func (s *scrobbler) Register(ctx context.Context, playerId int, trackId string,
err = s.ds.Artist(ctx).IncPlayCount(mf.ArtistID, playTime)
return err
})
if err != nil {
log.Error("Error while scrobbling", "trackId", trackId, err)
} else {
log.Info("Scrobbled", "title", mf.Title, "artist", mf.Artist, "user", userName(ctx))
}
return mf, err
}
@@ -53,9 +60,11 @@ func (s *scrobbler) NowPlaying(ctx context.Context, playerId int, playerName, tr
}
if mf == nil {
return nil, errors.New(fmt.Sprintf(`ID "%s" not found`, trackId))
return nil, fmt.Errorf(`ID "%s" not found`, trackId)
}
log.Info("Now Playing", "title", mf.Title, "artist", mf.Artist, "user", userName(ctx))
info := &NowPlayingInfo{TrackID: trackId, Username: username, Start: time.Now(), PlayerId: playerId, PlayerName: playerName}
return mf, s.npRepo.Enqueue(info)
}

View File

@@ -7,7 +7,7 @@ import (
"fmt"
"strings"
"github.com/deluan/navidrome/engine/auth"
"github.com/deluan/navidrome/core/auth"
"github.com/deluan/navidrome/model"
)

View File

@@ -3,7 +3,7 @@ package engine
import (
"context"
"github.com/deluan/navidrome/engine/auth"
"github.com/deluan/navidrome/core/auth"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/persistence"
. "github.com/onsi/ginkgo"

View File

@@ -1,10 +1,11 @@
package engine
import "github.com/google/wire"
import (
"github.com/google/wire"
)
var Set = wire.NewSet(
NewBrowser,
NewCover,
NewListGenerator,
NewPlaylists,
NewRatings,
@@ -12,5 +13,5 @@ var Set = wire.NewSet(
NewSearch,
NewNowPlayingRepository,
NewUsers,
NewMediaStreamer,
NewPlayers,
)

View File

@@ -10,7 +10,11 @@
#
# This script does not handle file names that contain spaces.
gofmtcmd=`which goimports || echo "gofmt"`
if which goimports > /dev/null; then
gofmtcmd=goimports
else
gofmtcmd=gofmt
fi
gofiles=$(git diff --cached --name-only --diff-filter=ACM | grep '.go$')
[ -z "$gofiles" ] && exit 0
@@ -20,7 +24,7 @@ unformatted=$($gofmtcmd -l $gofiles)
# Some files are not gofmt'd. Print message and fail.
echo >&2 "Go files must be formatted with $gofmcmd. Please run:"
echo >&2 "Go files must be formatted with '$gofmtcmd'. Please run:"
for fn in $unformatted; do
echo >&2 " $gofmtcmd -w $PWD/$fn"
done

9
git/pre-push Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
set -e
echo "#### Linting"
golangci-lint run -v
echo
echo "#### Running tests"
make test

59
go.mod
View File

@@ -1,42 +1,47 @@
module github.com/deluan/navidrome
go 1.13
go 1.14
require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/Masterminds/squirrel v1.2.0
github.com/astaxie/beego v1.12.1
github.com/Masterminds/squirrel v1.4.0
github.com/astaxie/beego v1.12.2
github.com/bradleyjkemp/cupaloy v2.3.0+incompatible
github.com/deluan/rest v0.0.0-20200114062534-0653ffe9eab4
github.com/deluan/rest v0.0.0-20200327222046-b71e558c45d0
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dhowden/tag v0.0.0-20191122115059-7e5c04feccd8
github.com/fatih/camelcase v0.0.0-20160318181535-f6a740d52f96 // indirect
github.com/fatih/structs v1.0.0 // indirect
github.com/go-chi/chi v4.0.3+incompatible
github.com/go-chi/cors v1.0.0
github.com/dhowden/tag v0.0.0-20200412032933-5d76b8eaae27
github.com/disintegration/imaging v1.6.2
github.com/djherbis/fscache v0.10.1
github.com/dustin/go-humanize v1.0.0
github.com/go-chi/chi v4.1.2+incompatible
github.com/go-chi/cors v1.1.1
github.com/go-chi/httprate v0.4.0
github.com/go-chi/jwtauth v4.0.4+incompatible
github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/golang/protobuf v1.3.1 // indirect
github.com/google/uuid v1.1.1
github.com/google/wire v0.4.0
github.com/kennygrant/sanitize v0.0.0-20170120101633-6a0bfdde8629
github.com/koding/multiconfig v0.0.0-20170327155832-26b6dfd3a84a
github.com/kr/pretty v0.1.0 // indirect
github.com/lib/pq v1.3.0 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5
github.com/onsi/ginkgo v1.12.0
github.com/onsi/gomega v1.9.0
github.com/microcosm-cc/bluemonday v1.0.3
github.com/mitchellh/mapstructure v1.3.2 // indirect
github.com/onsi/ginkgo v1.14.0
github.com/onsi/gomega v1.10.1
github.com/pelletier/go-toml v1.8.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pressly/goose v2.6.0+incompatible
github.com/sirupsen/logrus v1.4.2
github.com/smartystreets/assertions v1.0.1 // indirect
github.com/smartystreets/goconvey v1.6.4
github.com/stretchr/testify v1.4.0 // indirect
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 // indirect
golang.org/x/text v0.3.2 // indirect
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
github.com/sirupsen/logrus v1.6.0
github.com/spf13/afero v1.3.1 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/cobra v1.0.0
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.7.0
golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect
golang.org/x/text v0.3.3 // indirect
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/djherbis/atime.v1 v1.0.0 // indirect
gopkg.in/djherbis/stream.v1 v1.3.1 // indirect
gopkg.in/ini.v1 v1.57.0 // indirect
)
replace github.com/dhowden/tag => github.com/wader/tag v0.0.0-20200426234345-d072771f6a51

480
go.sum
View File

@@ -1,79 +1,213 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Masterminds/squirrel v1.2.0 h1:K1NhbTO21BWG47IVR0OnIZuE0LZcXAYqywrC3Ko53KI=
github.com/Masterminds/squirrel v1.2.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA=
github.com/OwnLocal/goes v1.0.0/go.mod h1:8rIFjBGTue3lCU0wplczcUgt9Gxgrkkrw7etMIcn8TM=
github.com/astaxie/beego v1.12.1 h1:dfpuoxpzLVgclveAXe4PyNKqkzgm5zF4tgF2B3kkM2I=
github.com/astaxie/beego v1.12.1/go.mod h1:kPBWpSANNbSdIqOc8SUL9h+1oyBMZhROeYsXQDbidWQ=
github.com/Masterminds/squirrel v1.4.0 h1:he5i/EXixZxrBUWcxzDYMiju9WZ3ld/l7QBNuo/eN3w=
github.com/Masterminds/squirrel v1.4.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/astaxie/beego v1.12.2 h1:CajUexhSX5ONWDiSCpeQBNVfTzOtPb9e9d+3vuU5FuU=
github.com/astaxie/beego v1.12.2/go.mod h1:TMcqhsbhN3UFpN+RCfysaxPAbrhox6QSS3NIAEp/uzE=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ=
github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
github.com/bradleyjkemp/cupaloy v2.3.0+incompatible h1:UafIjBvWQmS9i/xRg+CamMrnLTKNzo+bdmT/oH34c2Y=
github.com/bradleyjkemp/cupaloy v2.3.0+incompatible/go.mod h1:Au1Xw1sgaJ5iSFktEhYsS0dbQiS1B0/XMXl+42y9Ilk=
github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU=
github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
github.com/couchbase/go-couchbase v0.0.0-20181122212707-3e9b6e1258bb/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U=
github.com/couchbase/gomemcached v0.0.0-20181122193126-5125a94a666c/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U=
github.com/couchbase/gomemcached v0.0.0-20200526233749-ec430f949808/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c=
github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76 h1:Lgdd/Qp96Qj8jqLpq2cI1I1X7BJnu06efS+XkhRoLUQ=
github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deluan/rest v0.0.0-20200114062534-0653ffe9eab4 h1:VSoAcWJvj656TSyWbJ5KuGsi/J8dO5+iO9+5/7I8wao=
github.com/deluan/rest v0.0.0-20200114062534-0653ffe9eab4/go.mod h1:tSgDythFsl0QgS/PFWfIZqcJKnkADWneY80jaVRlqK8=
github.com/deluan/rest v0.0.0-20200327222046-b71e558c45d0 h1:qHX6TTBIsrpg0JkYNkoePIpstV37lhRVLj23bHUDNwk=
github.com/deluan/rest v0.0.0-20200327222046-b71e558c45d0/go.mod h1:tSgDythFsl0QgS/PFWfIZqcJKnkADWneY80jaVRlqK8=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dhowden/tag v0.0.0-20191122115059-7e5c04feccd8 h1:nmFnmD8VZXkjDHIb1Gnfz50cgzUvGN72zLjPRXBW/hU=
github.com/dhowden/tag v0.0.0-20191122115059-7e5c04feccd8/go.mod h1:SniNVYuaD1jmdEEvi+7ywb1QFR7agjeTdGKyFb0p7Rw=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/djherbis/fscache v0.10.1 h1:hDv+RGyvD+UDKyRYuLoVNbuRTnf2SrA2K3VyR1br9lk=
github.com/djherbis/fscache v0.10.1/go.mod h1:yyPYtkNnnPXsW+81lAcQS6yab3G2CRfnPLotBvtbf0c=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 h1:aaQcKT9WumO6JEJcRyTqFVq4XUZiUcKR2/GI31TOcz8=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI=
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/fatih/camelcase v0.0.0-20160318181535-f6a740d52f96 h1:5e8GDOdG6jKeeqNGbR+tlmqhf4vQVs3atTTMEWeEcAk=
github.com/fatih/camelcase v0.0.0-20160318181535-f6a740d52f96/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
github.com/fatih/structs v1.0.0 h1:BrX964Rv5uQ3wwS+KRUAJCBBw5PQmgJfJ6v4yly5QwU=
github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-chi/chi v4.0.3+incompatible h1:gakN3pDJnzZN5jqFV2TEdF66rTfKeITyR8qu6ekICEY=
github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/cors v1.0.0 h1:e6x8k7uWbUwYs+aXDoiUzeQFT6l0cygBYyNhD7/1Tg0=
github.com/go-chi/cors v1.0.0/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw=
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/cors v1.1.1 h1:eHuqxsIw89iXcWnWUN8R72JMibABJTN/4IOYI5WERvw=
github.com/go-chi/cors v1.1.1/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
github.com/go-chi/httprate v0.4.0 h1:M2qVV0w6ksgLs6L8lTrvqNeaVm0ZJNVdbYM8u2T8HaE=
github.com/go-chi/httprate v0.4.0/go.mod h1:7e7qjQtHzEbdyW5TYQrl4X2uNRCnlTajictc7B4ftgc=
github.com/go-chi/jwtauth v4.0.4+incompatible h1:LGIxg6YfvSBzxU2BljXbrzVc1fMlgqSKBQgKOGAVtPY=
github.com/go-chi/jwtauth v4.0.4+incompatible/go.mod h1:Q5EIArY/QnD6BdS+IyDw7B2m6iNbnPxtfd6/BcmtWbs=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.4.0 h1:kXcsA/rIGzJImVqPdhfnr6q0xsS9gU0515q1EPpJ9fE=
github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kennygrant/sanitize v0.0.0-20170120101633-6a0bfdde8629 h1:m1E9veL+2sjZOMSM7y3a6jJ9fNVaGyIJCXYDPm9U+/0=
github.com/kennygrant/sanitize v0.0.0-20170120101633-6a0bfdde8629/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
github.com/koding/multiconfig v0.0.0-20170327155832-26b6dfd3a84a h1:KZAp4Cn6Wybs23MKaIrKyb/6+qs2rncDspTuRYwOmvU=
github.com/koding/multiconfig v0.0.0-20170327155832-26b6dfd3a84a/go.mod h1:Y2SaZf2Rzd0pXkLVhLlCiAXFCLSXAIbTKDivVgff/AM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -83,103 +217,363 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 h1:BvoENQQU+fZ9uukda/RzCAL/191HHwJA5b13R6diVlY=
github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.3 h1:EjVH7OqbU219kdm8acbveoclh2zZFqPJTJw6VUlTLAQ=
github.com/microcosm-cc/bluemonday v1.0.3/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg=
github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
github.com/onsi/ginkgo v1.12.1 h1:mFwc4LvZ0xpSvDZ3E+k8Yte0hLOMxXUlP+yXtJqkYfQ=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw=
github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs=
github.com/peterh/liner v1.0.1-0.20171122030339-3681c2a91233/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pressly/goose v2.6.0+incompatible h1:3f8zIQ8rfgP9tyI0Hmcs2YNAqUCL1c+diLe3iU8Qd/k=
github.com/pressly/goose v2.6.0+incompatible/go.mod h1:m+QHWCqxR3k8D9l7qfzuC/djtlfzxr34mozWDYEu1z8=
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 h1:xT+JlYxNGqyT+XcU8iUrN18JYed2TvG9yN5ULG2jATM=
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
github.com/siddontang/ledisdb v0.0.0-20181029004158-becf5f38d373 h1:p6IxqQMjab30l4lb9mmkIkkcE1yv6o0SKbPhW5pxqHI=
github.com/siddontang/ledisdb v0.0.0-20181029004158-becf5f38d373/go.mod h1:mF1DpOSOUiJRMR+FDqaqu3EBqrybQtrDDszLUZ6oxPg=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.0 h1:wCi7urQOGBsYcQROHqpUUX4ct84xp40t9R9JX0FuA/U=
github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 h1:X+yvsM2yrEktyI+b2qND5gpH8YhURn0k8OCaeRnkINo=
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
github.com/siddontang/goredis v0.0.0-20150324035039-760763f78400/go.mod h1:DDcKzU3qCuvj/tPnimWSsZZzvk9qvkvrIL5naVBPh5s=
github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d h1:NVwnfyR3rENtlz62bcrkXME3INVUa4lcdGt+opvxExs=
github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w=
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.3.1 h1:GPTpEAuNr98px18yNQ66JllNil98wfRZ/5Ukny8FeQA=
github.com/spf13/afero v1.3.1/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c h1:3eGShk3EQf5gJCYW+WzA0TEJQd37HLOmlYF7N0YJwv0=
github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/wader/tag v0.0.0-20200426234345-d072771f6a51 h1:WAxntH7YQD6fIboAvewi7eU+2PQ7Y1K9OOXh67CM4bY=
github.com/wader/tag v0.0.0-20200426234345-d072771f6a51/go.mod h1:f3YqVk9PEeVf7T4JQ2+TdRqqjTg2fkaROZv0EMQOuKo=
github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/gopher-lua v0.0.0-20171031051903-609c9cd26973/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 h1:TFlARGu6Czu1z7q93HTxcP1P+/ZFC/IKythI5RzrnRg=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b h1:NVD8gBK33xpdqCaZVVtd6OFJp+3dxkXuz7+U7KaVN6s=
golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200117065230-39095c1d176c h1:FodBYPZKH5tAN2O60HlglMwXGAeV/4k+NKbli79M/2c=
golang.org/x/tools v0.0.0-20200117065230-39095c1d176c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/djherbis/atime.v1 v1.0.0 h1:eMRqB/JrLKocla2PBPKgQYg/p5UG4L6AUAs92aP7F60=
gopkg.in/djherbis/atime.v1 v1.0.0/go.mod h1:hQIUStKmJfvf7xdh/wtK84qe+DsTV5LnA9lzxxtPpJ8=
gopkg.in/djherbis/stream.v1 v1.3.1 h1:uGfmsOY1qqMjQQphhRBSGLyA9qumJ56exkRu9ASTjCw=
gopkg.in/djherbis/stream.v1 v1.3.1/go.mod h1:aEV8CBVRmSpLamVJfM903Npic1IKmb2qS30VAZ+sssg=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=

View File

@@ -24,6 +24,10 @@ const (
LevelTrace = Level(logrus.TraceLevel)
)
type contextKey string
const loggerCtxKey = contextKey("logger")
var (
currentLevel Level
defaultLogger = logrus.New()
@@ -66,7 +70,7 @@ func NewContext(ctx context.Context, keyValuePairs ...interface{}) context.Conte
}
logger := addFields(createNewLogger(), keyValuePairs)
ctx = context.WithValue(ctx, "logger", logger)
ctx = context.WithValue(ctx, loggerCtxKey, logger)
return ctx
}
@@ -176,10 +180,11 @@ func extractLogger(ctx interface{}) (*logrus.Entry, error) {
case *logrus.Entry:
return ctx, nil
case context.Context:
logger := ctx.Value("logger")
logger := ctx.Value(loggerCtxKey)
if logger != nil {
return logger.(*logrus.Entry), nil
}
return extractLogger(NewContext(ctx))
case *http.Request:
return extractLogger(ctx.Context())
}

View File

@@ -6,105 +6,175 @@ import (
"net/http/httptest"
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/sirupsen/logrus"
"github.com/sirupsen/logrus/hooks/test"
. "github.com/smartystreets/goconvey/convey"
)
func TestLog(t *testing.T) {
SetLevel(LevelInfo)
RegisterFailHandler(Fail)
RunSpecs(t, "Log Suite")
}
Convey("Test Logger", t, func() {
l, hook := test.NewNullLogger()
var _ = Describe("Logger", func() {
var l *logrus.Logger
var hook *test.Hook
BeforeEach(func() {
l, hook = test.NewNullLogger()
SetLevel(LevelInfo)
SetDefaultLogger(l)
})
Convey("Plain message", func() {
Context("Logging", func() {
It("logs a simple message", func() {
Error("Simple Message")
So(hook.LastEntry().Message, ShouldEqual, "Simple Message")
So(hook.LastEntry().Data, ShouldBeEmpty)
Expect(hook.LastEntry().Message).To(Equal("Simple Message"))
Expect(hook.LastEntry().Data).To(BeEmpty())
})
Convey("Passing nil as context", func() {
It("logs a message when context is nil", func() {
Error(nil, "Simple Message")
So(hook.LastEntry().Message, ShouldEqual, "Simple Message")
So(hook.LastEntry().Data, ShouldBeEmpty)
Expect(hook.LastEntry().Message).To(Equal("Simple Message"))
Expect(hook.LastEntry().Data).To(BeEmpty())
})
SkipConvey("Empty context", func() {
Error(context.Background(), "Simple Message")
So(hook.LastEntry().Message, ShouldEqual, "Simple Message")
So(hook.LastEntry().Data, ShouldBeEmpty)
It("Empty context", func() {
Error(context.TODO(), "Simple Message")
Expect(hook.LastEntry().Message).To(Equal("Simple Message"))
Expect(hook.LastEntry().Data).To(BeEmpty())
})
Convey("Message with two kv pairs", func() {
It("logs messages with two kv pairs", func() {
Error("Simple Message", "key1", "value1", "key2", "value2")
So(hook.LastEntry().Message, ShouldEqual, "Simple Message")
So(hook.LastEntry().Data["key1"], ShouldEqual, "value1")
So(hook.LastEntry().Data["key2"], ShouldEqual, "value2")
So(hook.LastEntry().Data, ShouldHaveLength, 2)
Expect(hook.LastEntry().Message).To(Equal("Simple Message"))
Expect(hook.LastEntry().Data["key1"]).To(Equal("value1"))
Expect(hook.LastEntry().Data["key2"]).To(Equal("value2"))
Expect(hook.LastEntry().Data).To(HaveLen(2))
})
Convey("Only error", func() {
It("logs error objects as simple messages", func() {
Error(errors.New("error test"))
So(hook.LastEntry().Message, ShouldEqual, "error test")
So(hook.LastEntry().Data, ShouldBeEmpty)
Expect(hook.LastEntry().Message).To(Equal("error test"))
Expect(hook.LastEntry().Data).To(BeEmpty())
})
Convey("Error as last argument", func() {
It("logs errors passed as last argument", func() {
Error("Error scrobbling track", "id", 1, errors.New("some issue"))
So(hook.LastEntry().Message, ShouldEqual, "Error scrobbling track")
So(hook.LastEntry().Data["id"], ShouldEqual, 1)
So(hook.LastEntry().Data["error"], ShouldEqual, "some issue")
So(hook.LastEntry().Data, ShouldHaveLength, 2)
Expect(hook.LastEntry().Message).To(Equal("Error scrobbling track"))
Expect(hook.LastEntry().Data["id"]).To(Equal(1))
Expect(hook.LastEntry().Data["error"]).To(Equal("some issue"))
Expect(hook.LastEntry().Data).To(HaveLen(2))
})
Convey("Passing a request", func() {
ctx := NewContext(nil, "foo", "bar")
It("can get data from the request's context", func() {
ctx := NewContext(context.TODO(), "foo", "bar")
req := httptest.NewRequest("get", "/", nil).WithContext(ctx)
Error(req, "Simple Message", "key1", "value1")
So(hook.LastEntry().Message, ShouldEqual, "Simple Message")
So(hook.LastEntry().Data["foo"], ShouldEqual, "bar")
So(hook.LastEntry().Data["key1"], ShouldEqual, "value1")
So(hook.LastEntry().Data, ShouldHaveLength, 2)
Expect(hook.LastEntry().Message).To(Equal("Simple Message"))
Expect(hook.LastEntry().Data["foo"]).To(Equal("bar"))
Expect(hook.LastEntry().Data["key1"]).To(Equal("value1"))
Expect(hook.LastEntry().Data).To(HaveLen(2))
})
Convey("Skip if level is lower", func() {
It("does not log anything if level is lower", func() {
SetLevel(LevelError)
Info("Simple Message")
So(hook.LastEntry(), ShouldBeNil)
Expect(hook.LastEntry()).To(BeNil())
})
It("logs source file and line number, if requested", func() {
SetLogSourceLine(true)
Error("A crash happened")
Expect(hook.LastEntry().Message).To(Equal("A crash happened"))
// NOTE: This assertions breaks if the line number changes
Expect(hook.LastEntry().Data[" source"]).To(ContainSubstring("/log/log_test.go:92"))
})
})
Convey("Test extractLogger", t, func() {
Convey("It returns an error if the context is nil", func() {
Context("Levels", func() {
BeforeEach(func() {
SetLevel(LevelTrace)
})
It("logs error messages", func() {
Error("msg")
Expect(hook.LastEntry().Level).To(Equal(logrus.ErrorLevel))
})
It("logs warn messages", func() {
Warn("msg")
Expect(hook.LastEntry().Level).To(Equal(logrus.WarnLevel))
})
It("logs info messages", func() {
Info("msg")
Expect(hook.LastEntry().Level).To(Equal(logrus.InfoLevel))
})
It("logs debug messages", func() {
Debug("msg")
Expect(hook.LastEntry().Level).To(Equal(logrus.DebugLevel))
})
It("logs info messages", func() {
Trace("msg")
Expect(hook.LastEntry().Level).To(Equal(logrus.TraceLevel))
})
})
Context("extractLogger", func() {
It("returns an error if the context is nil", func() {
_, err := extractLogger(nil)
So(err, ShouldNotBeNil)
Expect(err).ToNot(BeNil())
})
Convey("It returns an error if the context is a string", func() {
It("returns an error if the context is a string", func() {
_, err := extractLogger("any msg")
So(err, ShouldNotBeNil)
Expect(err).ToNot(BeNil())
})
Convey("It returns the logger from context if it has one", func() {
It("returns the logger from context if it has one", func() {
logger := logrus.NewEntry(logrus.New())
ctx := context.Background()
ctx = context.WithValue(ctx, "logger", logger)
ctx = context.WithValue(ctx, loggerCtxKey, logger)
l, err := extractLogger(ctx)
So(err, ShouldBeNil)
So(l, ShouldEqual, logger)
Expect(extractLogger(ctx)).To(Equal(logger))
})
Convey("It returns the logger from request's context if it has one", func() {
It("returns the logger from request's context if it has one", func() {
logger := logrus.NewEntry(logrus.New())
ctx := context.Background()
ctx = context.WithValue(ctx, "logger", logger)
ctx = context.WithValue(ctx, loggerCtxKey, logger)
req := httptest.NewRequest("get", "/", nil).WithContext(ctx)
l, err := extractLogger(req)
So(err, ShouldBeNil)
So(l, ShouldEqual, logger)
Expect(extractLogger(req)).To(Equal(logger))
})
})
}
Context("SetLevelString", func() {
It("converts Critical level", func() {
SetLevelString("Critical")
Expect(CurrentLevel()).To(Equal(LevelCritical))
})
It("converts Error level", func() {
SetLevelString("ERROR")
Expect(CurrentLevel()).To(Equal(LevelError))
})
It("converts Warn level", func() {
SetLevelString("warn")
Expect(CurrentLevel()).To(Equal(LevelWarn))
})
It("converts Info level", func() {
SetLevelString("info")
Expect(CurrentLevel()).To(Equal(LevelInfo))
})
It("converts Debug level", func() {
SetLevelString("debug")
Expect(CurrentLevel()).To(Equal(LevelDebug))
})
It("converts Trace level", func() {
SetLevelString("trace")
Expect(CurrentLevel()).To(Equal(LevelTrace))
})
})
})

18
main.go
View File

@@ -1,21 +1,7 @@
package main
import (
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/db"
)
import "github.com/deluan/navidrome/cmd"
func main() {
if !conf.Server.DevDisableBanner {
println(consts.Banner())
}
conf.Load()
db.EnsureLatestVersion()
a := CreateServer(conf.Server.MusicFolder)
a.MountRouter("/rest", CreateSubsonicAPIRouter())
a.MountRouter("/app", CreateAppRouter("/app"))
a.Run(":" + conf.Server.Port)
cmd.Execute()
}

View File

@@ -3,27 +3,30 @@ package model
import "time"
type Album struct {
ID string `json:"id" orm:"column(id)"`
Name string `json:"name"`
ArtistID string `json:"artistId" orm:"pk;column(artist_id)"`
CoverArtPath string `json:"coverArtPath"`
CoverArtId string `json:"coverArtId"`
Artist string `json:"artist"`
AlbumArtist string `json:"albumArtist"`
Year int `json:"year"`
Compilation bool `json:"compilation"`
SongCount int `json:"songCount"`
Duration int `json:"duration"`
Genre string `json:"genre"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Annotations
// Annotations
PlayCount int `json:"-" orm:"-"`
PlayDate time.Time `json:"-" orm:"-"`
Rating int `json:"-" orm:"-"`
Starred bool `json:"-" orm:"-"`
StarredAt time.Time `json:"-" orm:"-"`
ID string `json:"id" orm:"column(id)"`
Name string `json:"name"`
CoverArtPath string `json:"coverArtPath"`
CoverArtId string `json:"coverArtId"`
ArtistID string `json:"artistId" orm:"pk;column(artist_id)"`
Artist string `json:"artist"`
AlbumArtistID string `json:"albumArtistId" orm:"pk;column(album_artist_id)"`
AlbumArtist string `json:"albumArtist"`
MaxYear int `json:"maxYear"`
MinYear int `json:"minYear"`
Compilation bool `json:"compilation"`
SongCount int `json:"songCount"`
Duration float32 `json:"duration"`
Genre string `json:"genre"`
FullText string `json:"fullText"`
SortAlbumName string `json:"sortAlbumName"`
SortArtistName string `json:"sortArtistName"`
SortAlbumArtistName string `json:"sortAlbumArtistName"`
OrderAlbumName string `json:"orderAlbumName"`
OrderAlbumArtistName string `json:"orderAlbumArtistName"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type Albums []Album
@@ -31,14 +34,16 @@ type Albums []Album
type AlbumRepository interface {
CountAll(...QueryOptions) (int64, error)
Exists(id string) (bool, error)
Put(m *Album) error
Get(id string) (*Album, error)
FindByArtist(artistId string) (Albums, error)
FindByArtist(albumArtistId string) (Albums, error)
GetAll(...QueryOptions) (Albums, error)
GetRandom(...QueryOptions) (Albums, error)
GetStarred(options ...QueryOptions) (Albums, error)
Search(q string, offset int, size int) (Albums, error)
Refresh(ids ...string) error
PurgeEmpty() error
AnnotatedRepository
}
func (a Album) GetAnnotations() Annotations {
return a.Annotations
}

View File

@@ -2,8 +2,24 @@ package model
import "time"
type Annotations struct {
PlayCount int64 `json:"playCount"`
PlayDate time.Time `json:"playDate"`
Rating int `json:"rating"`
Starred bool `json:"starred"`
StarredAt time.Time `json:"starredAt"`
}
type AnnotatedModel interface {
GetAnnotations() Annotations
}
type AnnotatedRepository interface {
IncPlayCount(itemID string, ts time.Time) error
SetStar(starred bool, itemIDs ...string) error
SetRating(rating int, itemID string) error
}
// While I can't find a better way to make these fields optional in the models, I keep this list here
// to be used in other packages
var AnnotationFields = []string{"playCount", "playDate", "rating", "starred", "starredAt"}

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