Compare commits

...

57 Commits

Author SHA1 Message Date
Caio Cotts
fee0f40a52 Bump dependencies 2023-11-16 20:38:45 -05:00
Caio Cotts
9d2aaff8cb Bump golang.org/x/tools from 0.13.0 to 0.15.0 2023-11-16 20:19:29 -05:00
Caio Cotts
2ff4023cce Bump golang.org/x/image from 0.12.0 to 0.14.0 2023-11-16 20:16:25 -05:00
Kendall Garner
79870b1090 Do not empty old artist metadata (#2423) 2023-11-16 19:20:37 -05:00
Kendall Garner
7a858a2db3 Fix external link for artist page if LastFM is missinb but Musicbrainz is not (#2533)
* fix mbz link if lastfm does not exist

* use lastfmUrl field

* fix artist info undefined
2023-11-16 19:07:52 -05:00
dependabot[bot]
9cefaf66a4 Bump github.com/onsi/gomega from 1.29.0 to 1.30.0
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.29.0 to 1.30.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.29.0...v1.30.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-16 18:45:21 -05:00
Kendall Garner
3debd31b12 Add more replaygain tests, fix wma (#2356)
* add more replaygain tests, fix wma

* Convert individual specs to a table spec

* Fix pipeline, by commenting incompatible tests

---------

Co-authored-by: Deluan <deluan@navidrome.org>
2023-11-14 20:25:18 -05:00
Deluan Quintão
24d9fb5b48 Update translations (#2409)
Co-authored-by: deluan <deluan@users.noreply.github.com>
2023-11-14 19:21:26 -05:00
certuna
40841ab917 Small date mapping fix (#2584)
* Update mapping.go

fallback in the case there's no Date tagged but Original Date or Release Date are tagged

* Add tests

---------

Co-authored-by: Deluan <deluan@navidrome.org>
2023-11-11 17:13:07 -05:00
certuna
bae5fc946b Fix hardcoded IPv4 literals (#2602)
* Update server_test.go

no hardcoded IPv4 literals

* Update package.json

no hardcoded IPv4 literals
2023-11-11 16:46:53 -05:00
Deluan
e055826068 Fix devcontainer for Go 1.21 2023-11-09 18:23:44 -05:00
dependabot[bot]
54bde266b4 Bump github.com/mattn/go-sqlite3 from 1.14.16 to 1.14.18
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.16 to 1.14.18.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.16...v1.14.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-07 18:24:08 -05:00
dependabot[bot]
3a7376901b Bump golang.org/x/sync from 0.3.0 to 0.5.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.3.0 to 0.5.0.
- [Commits](https://github.com/golang/sync/compare/v0.3.0...v0.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-07 18:19:32 -05:00
dependabot[bot]
de3d870100 Bump github.com/spf13/cobra from 1.7.0 to 1.8.0
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.7.0...v1.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-07 18:08:13 -05:00
certuna
03175e1a9d Use file creation date for Date Added/CreatedAt (#2553)
* Update mapping.go

CreatedAt = BirthTime

* Update metadata.go

Add BirthTime() function

* Update spread_fs.go

Replacing djherbis/atime package with djherbis/times, as times includes the functionality of atime

* Update go.mod

remove djherbis/atime, add djherbis/times

* Update mapping.go

time package not used anymore

* Update go.sum

removed djherbis/atime, added djherbis/times

* Update spread_fs.go

revert to previous, cannot get rid of /atime after all since it's a dependency of /fscache

* Update go.mod

djherbis/times 1.6.0 now released

* Update go.sum

new sums

* Update metadata.go

Inverted if statement, more readable

* Update go.mod

format fix

* Update go.sum

format fix

* Update go.sum

format fix

* Update go.sum

format fix

* Update metadata.go

variable name times -> fileProperties
check for errors

* Update metadata.go

reverse order of error check

* Update metadata.go

typo

* Update metadata.go

https://github.com/navidrome/navidrome/pull/2553#issuecomment-1787967615
2023-11-01 16:41:07 -04:00
Sam Watson
26472f46fe POST endpoint for importing m3u playlists - #2078 (#2273)
* wip: API endpoint for creating playlists from m3u files

* wip: get user id from context

* temporarily disable failing test

* custom logic for playlist route to accomodate m3u content type

* incorporate playlist parsing into existing logic in core

* re-enable test

* fix locally failing test

* Address requested changes.

* Improve ImportFile tests.

* Remove ownerID as a parameter of ImportM3U.

* Write tests for ImportM3U.

* Separate ImportM3U test into two.

* Test OwnerID and playlist Name.

---------

Co-authored-by: Sam Watson <SwatsonCodes@users.noreply.github.com>
Co-authored-by: caiocotts <caio@cotts.com.br>
2023-11-01 14:59:47 -04:00
dependabot[bot]
6bca7531aa Bump @babel/traverse from 7.19.3 to 7.23.2 in /ui
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.19.3 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-31 17:46:10 -04:00
dependabot[bot]
68d1d5c99f Bump github.com/lestrrat-go/jwx/v2 from 2.0.12 to 2.0.16
Bumps [github.com/lestrrat-go/jwx/v2](https://github.com/lestrrat-go/jwx) from 2.0.12 to 2.0.16.
- [Release notes](https://github.com/lestrrat-go/jwx/releases)
- [Changelog](https://github.com/lestrrat-go/jwx/blob/develop/v2/Changes)
- [Commits](https://github.com/lestrrat-go/jwx/compare/v2.0.12...v2.0.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-31 17:27:45 -04:00
dependabot[bot]
db6c46091e Bump github.com/beego/beego/v2 from 2.0.7 to 2.1.3
Bumps [github.com/beego/beego/v2](https://github.com/beego/beego) from 2.0.7 to 2.1.3.
- [Release notes](https://github.com/beego/beego/releases)
- [Changelog](https://github.com/beego/beego/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/beego/beego/compare/v2.0.7...v2.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-31 17:24:55 -04:00
dependabot[bot]
4cd916bb78 Bump react-router-dom from 5.3.0 to 5.3.4 in /ui
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 5.3.0 to 5.3.4.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/v5.3.4/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-31 16:55:59 -04:00
dependabot[bot]
c40e83efab Bump github.com/go-chi/chi/v5 from 5.0.8 to 5.0.10
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.8 to 5.0.10.
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.0.8...v5.0.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-31 16:44:35 -04:00
Stephan Wahlen
9094f41f25 Improve TopSongs findMatchingTrack by de-prioritizing compilations (#2532)
in reference to https://github.com/navidrome/navidrome/issues/1701
2023-10-31 16:00:53 -04:00
dependabot[bot]
9ff95b6ced Bump github.com/onsi/gomega from 1.27.10 to 1.29.0
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.27.10 to 1.29.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.27.10...v1.29.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-31 15:48:16 -04:00
Kendall Garner
77ace8570c Coalesce genre to null for smart playlist (#2573) 2023-10-31 13:22:57 -04:00
Matthias Schmidt
59f0c487e7 Jukebox cleanup (#2554)
* Fixing typo FFmpegPath -> MPVPath

* Fixing panic by applying afontenot patch

* Using mpv audio-device flag and naming for config and playback
2023-10-17 18:12:48 -04:00
Deluan
2cd4358172 Make Jukebox available to Subsonic clients 2023-09-14 20:15:39 -04:00
dependabot[bot]
248bf232ff Bump github.com/lestrrat-go/jwx/v2 from 2.0.11 to 2.0.12 (#2480)
Bumps [github.com/lestrrat-go/jwx/v2](https://github.com/lestrrat-go/jwx) from 2.0.11 to 2.0.12.
- [Release notes](https://github.com/lestrrat-go/jwx/releases)
- [Changelog](https://github.com/lestrrat-go/jwx/blob/develop/v2/Changes)
- [Commits](https://github.com/lestrrat-go/jwx/compare/v2.0.11...v2.0.12)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-10 18:06:51 -04:00
dependabot[bot]
b5664ab905 Bump github.com/onsi/ginkgo/v2 from 2.11.0 to 2.12.0 (#2497)
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.11.0 to 2.12.0.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.11.0...v2.12.0)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-10 12:28:43 -04:00
Lukas H
ac7f94e620 Fix text being unreadable with Ligera theme (#2517)
Change MuiFormGroup color to make it readable.
2023-09-10 12:28:21 -04:00
dependabot[bot]
d45f9f172d Bump github.com/google/uuid from 1.3.0 to 1.3.1 (#2489)
Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/google/uuid/releases)
- [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/uuid/compare/v1.3.0...v1.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-10 12:05:58 -04:00
dependabot[bot]
250107d668 Bump golang.org/x/image from 0.9.0 to 0.12.0 (#2507)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.9.0 to 0.12.0.
- [Commits](https://github.com/golang/image/compare/v0.9.0...v0.12.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-10 12:05:28 -04:00
BoniK
64b14db55a Add Korean Language (#2463) 2023-09-10 11:52:18 -04:00
dependabot[bot]
73d1851c0d Bump golang.org/x/tools from 0.9.1 to 0.13.0 (#2516)
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.9.1 to 0.13.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.9.1...v0.13.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-10 11:48:33 -04:00
Matthias Schmidt
1b16e1140f Jukebox mode (#2289)
* Adding cache directory to ignore-list

* Adding jukebox-related config options

* Adding DevEnableJukebox config option pls. dummy server

* Adding types and routers

* Now without panic

* First draft on parsing the action

* Some cleanups

* Adding playback server

* Verify audio device configuration

* Adding debug-build target to have full symbol support

* Adding beep sound library pls some example code. Not working yet

* Play a fixed mp3 on any interface access for testing purposes

* Put action code into separate file, adding stringer, more debug output, prepare structs, validation

* Put action parameter parser code where it belongs

* Have a single Action transporting all information

* User fmt.Errorf for error-generation

* Adding wide playback interface

* Use action map for parsing, stringer instead switch stmt.

* Use but only one switch case and direct dispatch, refactoring

* Add error handling and pushing to client

* send decent errormessage, no internal server error

* Adding playback devices slice and load it from config

* Combine config-verification and structure init

* Return user-specific device

* Separate playback server from device

* Use dataStore to retrieve mediafile by id

* WIP: Playlist and start/stop handling. Doing start/stop the hard way as of now

* WIP: set, start and stop work on one single song. More to come

* Dont need to wait for the end

* Merge jukebox_action.go into jukebox.go

* Remove getParameterAsInt64(). Use existing requiredParamInt() instead

* Dont need to call newFailure() explicitly

* Remove int64, use int instead.

* Add and set action now accept multiple ids

* Kickout copy of childFromMediaFile(). It is not needed here.

* Refactoring devices and playbackServer

* Turn (internal) playback.DeviceStatus into subsonic JukeboxStatus when rendering output. Indexes int64 -> int

* Now we have a position and playing status

* Switching gain to float32, xs:float is defined as 32 bit. Fixing nasty copy/pointer bug

* Now with volume control

* Start working the queue

* Remove user from device interface

* Rename function GetDevice -> GetDeviceForUser to make intention clearer

* Have a nice stringer for the queue

* User Prepared boolean for now to allow pause/unpause

* Skipping works, but without offsets

* Make ChildFromMediaFile public to be used in jukebox get() implementation

* Return position in seconds and implement offset-skip in seconds

* Default offset to 0

* Adding a simple setGain implementation

* Prepare for transcoding AAC

* WIP: transcode to WAV to use beeps wav decoder. Not done yet.

* WIP: out of sheer desparation: convert to MP3 (which works) rather than WAV to troubleshoot issue.

* Use FLAC as intermediate format to play Apple AAC

* A bit of cleanup

* Catching the end-of-stream event for further reactions

* Have a trackSwitching goroutine waiting on channel when track ends

* Move decoder code into own file. Restructure code a bit

* Now with going on to play the next song in the playlist

* Adding shuffle feature

* Implementing remove action

* Cleanup code

* Remove templates for ffmpeg mp3 generation. Not needed anymore.

* Adding some documentation

* Check whether offset into track is in range. Fixing potential remove track bug. Documentation

* Make golangci-lint happy: handling return values

* Adding test suite and example dummy for playback package

* Adding some basic queue tests

* Only use Jukebox.Enabled config option

* Adding stream closing handling

* Pass context.Context to all PlaybackDevice methods

* Remove unneeded function

* Correct spelling

* Reduce visibility of ChildFromMediaFile

* Decomplicate action-parsing

* Adding simple tempfile-based AAC->FLAC transcoding. No parallel reading and writing yet.

* Try to optimize pipe-writing, tempfile-handling and reading. Not done yet.

* Do a synchronous copy of the tempfile. Racecondition detected

* More debugging statements and fixing the play/pause bug. More work needed

* Start the trackSwitcher() with each device once. Return JSON position even if its 0. More debug-output

* Moving all track-handling code into own module

* Fix typo. Do not pass ctx around when not applicable

* WIP: More refactoring, debugging output

* Fix nil pointer

* Repairing MP3 playback by pinning indirect dependencies: hajimehoshi/go-mp3 and hajimehoshi/oto

* Do not forget to cleanup after a skip action

* Make resync with master easy

* Adding missing mocks

* Adding missing error-handling found by linter

* Updating github.com/hajimehoshi/oto

* Removing duplicate function

* Move BEEP-related code into own package

* Juggle beep-related code around as preparation for interface access

* More refactoring for interface separation

* Gather CloseDevice() behind Track interface.

* Adding skeleton, draft audio-interface using mpv.io

* Adding majority of interface commands using messages to mpv socket.

* Adding end-of-stream handling

* MPV: start/stop are working

* postition is given in float in mpv

* Unify Close() and CloseDevice(). Using temp filename for controlling socket

* Wait until control-socket shows up. Cleanup socket in Close()

* Use canceable command. Rename to Executor

* Skipping tracks works now

* Now with actually setting the position

* Fix regain

* Add missing error-handling found by linter

* Adding retry mode on time-pos property getter

* Remove unneeded code on queue

* Putting build-tag beep onto beep files

* Remove deprecated call to rand.Seed()

"As of Go 1.20 there is no reason to call Seed with a random value. Programs that call Seed with a known value to get a specific sequence of results should use New(NewSource(seed)) to obtain a local random generator."

* Using int32 to conform to Subsonic API spec

* Fix merge error

* Minor style changes

* Get username from context

---------

Co-authored-by: Deluan <deluan@navidrome.org>
2023-09-10 11:25:22 -04:00
Deluan Quintão
f941347cf1 Upgrade to Go 1.21 (#2475)
* Upgrade to Go 1.21

* Remove 'replacements' from goreleaser config
2023-08-09 11:39:49 -04:00
dependabot[bot]
1b5cefdada Bump github.com/onsi/gomega from 1.27.8 to 1.27.9 (#2450)
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.27.8 to 1.27.9.
- [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.27.8...v1.27.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-22 19:21:39 -04:00
dependabot[bot]
4cf25fc611 Bump github.com/microcosm-cc/bluemonday from 1.0.24 to 1.0.25 (#2449)
Bumps [github.com/microcosm-cc/bluemonday](https://github.com/microcosm-cc/bluemonday) from 1.0.24 to 1.0.25.
- [Release notes](https://github.com/microcosm-cc/bluemonday/releases)
- [Commits](https://github.com/microcosm-cc/bluemonday/compare/v1.0.24...v1.0.25)

---
updated-dependencies:
- dependency-name: github.com/microcosm-cc/bluemonday
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-22 19:11:15 -04:00
dependabot[bot]
14ba83ea1b Bump github.com/go-chi/chi/v5 from 5.0.8 to 5.0.10 (#2444)
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.8 to 5.0.10.
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.0.8...v5.0.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-21 19:12:56 -04:00
dependabot[bot]
08f3fd1343 Bump github.com/pressly/goose/v3 from 3.13.1 to 3.13.4 (#2442)
Bumps [github.com/pressly/goose/v3](https://github.com/pressly/goose) from 3.13.1 to 3.13.4.
- [Release notes](https://github.com/pressly/goose/releases)
- [Changelog](https://github.com/pressly/goose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pressly/goose/compare/v3.13.1...v3.13.4)

---
updated-dependencies:
- dependency-name: github.com/pressly/goose/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2023-07-21 19:11:59 -04:00
dependabot[bot]
3d66f58725 Bump tough-cookie from 4.1.2 to 4.1.3 in /ui (#2441)
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.1.2...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-21 19:04:32 -04:00
dependabot[bot]
5b1ba3df05 Bump word-wrap from 1.2.3 to 1.2.4 in /ui (#2446)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-21 19:02:10 -04:00
Deluan
a002830775 Fix EnableMediaFileCoverArt option default value 2023-07-10 18:07:58 -04:00
dependabot[bot]
7b600bed05 Bump golang.org/x/tools from 0.10.0 to 0.11.0 (#2432)
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.10.0 to 0.11.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.10.0...v0.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-05 16:38:32 -04:00
dependabot[bot]
7d0a1916d8 Bump golang.org/x/image from 0.8.0 to 0.9.0 (#2430)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.8.0 to 0.9.0.
- [Commits](https://github.com/golang/image/compare/v0.8.0...v0.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-05 16:34:58 -04:00
dependabot[bot]
c7fe311c7f Bump github.com/go-chi/jwtauth/v5 from 5.1.0 to 5.1.1 (#2427)
Bumps [github.com/go-chi/jwtauth/v5](https://github.com/go-chi/jwtauth) from 5.1.0 to 5.1.1.
- [Release notes](https://github.com/go-chi/jwtauth/releases)
- [Commits](https://github.com/go-chi/jwtauth/compare/v5.1.0...v5.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-05 16:31:38 -04:00
dependabot[bot]
4520a34648 Bump github.com/pressly/goose/v3 from 3.11.2 to 3.13.1 (#2428)
Bumps [github.com/pressly/goose/v3](https://github.com/pressly/goose) from 3.11.2 to 3.13.1.
- [Release notes](https://github.com/pressly/goose/releases)
- [Changelog](https://github.com/pressly/goose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pressly/goose/compare/v3.11.2...v3.13.1)

---
updated-dependencies:
- dependency-name: github.com/pressly/goose/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-05 16:31:22 -04:00
BenzLeung
3e14c3c4f8 Add support for lyrics tag unsynced lyrics (#2391)
* Add support for lyrics tag `unsynced_lyrics`

* Update metadata.go

* Update metadata.go

resolve lint issue

* format the code with `goimports`

format the code with `goimports`
2023-06-20 09:32:49 -04:00
dependabot[bot]
1e891d6b07 Bump github.com/prometheus/client_golang from 1.15.1 to 1.16.0 (#2408)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.15.1 to 1.16.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.15.1...v1.16.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-19 14:23:36 -04:00
dependabot[bot]
caf9b22d35 Bump golang.org/x/image from 0.7.0 to 0.8.0 (#2407)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.7.0 to 0.8.0.
- [Commits](https://github.com/golang/image/compare/v0.7.0...v0.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-19 14:23:01 -04:00
Deluan Quintão
4f8742bcd1 Update translations (#2329)
Co-authored-by: deluan <deluan@users.noreply.github.com>
2023-06-19 12:27:56 -04:00
Deluan
26aa0f4fff Fix typo 2023-06-19 11:19:43 -04:00
Deluan
4898f31f6d Add format target to Makefile 2023-06-19 11:15:51 -04:00
Philipp Wolfer
9da013f339 Submit duration to ListenBrainz (#2405) 2023-06-17 12:27:00 -04:00
Deluan
5af67c78af Upgrade Go dependencies 2023-06-16 22:34:02 -04:00
Philipp Wolfer
c8608956be Fix listenbrainz submission and clarify MusicBrainz recording ID field (#2279)
* Fix MB recording ID parameter name for ListenBrainz submission

This follows the ListenBrainz API documentation.

Fixes #1657

* Rename MediaFile.MbzTrackID to MbzRecordingID

This better reflects the actual data. That the MusicBrainz
recording ID is stored in file metadata as musicbrainz_trackid
is a historical artifact.

* Rename database column mbz_track_id to mbz_recording_id
2023-06-16 18:00:01 -04:00
Deluan
36eda871f6 Fix locale-dependent tests. Closes #2402 2023-06-16 16:38:03 -04:00
David Casado
7c92a73208 Ignore playlists starting with a dot - #2367 (#2390) 2023-06-16 15:55:17 -04:00
105 changed files with 3268 additions and 675 deletions

View File

@@ -2,7 +2,7 @@
# [Choice] Go version: 1, 1.15, 1.14
ARG VARIANT="1"
FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT}
FROM mcr.microsoft.com/vscode/devcontainers/go:${VARIANT}
# [Option] Install Node.js
ARG INSTALL_NODE="true"
@@ -17,4 +17,4 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# RUN go get -x <your-dependency-or-tool>
# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1

View File

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

View File

@@ -16,10 +16,10 @@ jobs:
- name: Install taglib
run: sudo apt-get install libtag1-dev
- name: Set up Go 1.20
- name: Set up Go 1.21
uses: actions/setup-go@v3
with:
go-version: 1.20.x
go-version: 1.21.x
- uses: actions/checkout@v3
@@ -39,7 +39,7 @@ jobs:
run: |
git status --porcelain
if [ -n "$(git status --porcelain)" ]; then
echo 'To fix this check, run "goimports -w $(find . -name '*.go' | grep -v '_gen.go$') && go mod tidy"'
echo 'To fix this check, run "make format" and commit the changes'
exit 1
fi
@@ -48,7 +48,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go_version: [1.20.x,1.19.x]
go_version: [1.21.x,1.20.x]
steps:
- name: Install taglib
run: sudo apt-get install libtag1-dev
@@ -126,7 +126,7 @@ jobs:
path: ui/build
- name: Config /github/workspace folder as trusted
uses: docker://deluan/ci-goreleaser:1.20.3-1
uses: docker://deluan/ci-goreleaser:1.21.0-1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -134,7 +134,7 @@ jobs:
- name: Run GoReleaser - SNAPSHOT
if: startsWith(github.ref, 'refs/tags/') != true
uses: docker://deluan/ci-goreleaser:1.20.3-1
uses: docker://deluan/ci-goreleaser:1.21.0-1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -142,7 +142,7 @@ jobs:
- name: Run GoReleaser - RELEASE
if: startsWith(github.ref, 'refs/tags/')
uses: docker://deluan/ci-goreleaser:1.20.3-1
uses: docker://deluan/ci-goreleaser:1.21.0-1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

View File

@@ -1,5 +1,5 @@
run:
go: "1.19"
go: "1.20"
linters:
enable:

View File

@@ -116,12 +116,6 @@ archives:
- format_overrides:
- goos: windows
format: zip
replacements:
darwin: macOS
linux: Linux
windows: Windows
386: i386
amd64: x86_64
checksum:
name_template: "{{ .ProjectName }}_checksums.txt"

View File

@@ -9,7 +9,7 @@ GIT_SHA=source_archive
GIT_TAG=$(patsubst navidrome-%,v%,$(notdir $(PWD)))
endif
CI_RELEASER_VERSION=1.20.3-1 ## https://github.com/navidrome/ci-goreleaser
CI_RELEASER_VERSION=1.21.0-1 ## https://github.com/navidrome/ci-goreleaser
setup: check_env download-deps setup-git ##@1_Run_First Install dependencies and prepare development environment
@echo Downloading Node dependencies...
@@ -45,6 +45,12 @@ lintall: lint ##@Development Lint Go and JS code
@(cd ./ui && npm run lint)
.PHONY: lintall
format: ##@Development Format code
@(cd ./ui && npm run prettier)
@go run golang.org/x/tools/cmd/goimports -w `find . -name '*.go' | grep -v _gen.go$$`
@go mod tidy
.PHONY: format
wire: check_go_env ##@Development Update Dependency Injection
go run github.com/google/wire/cmd/wire@latest ./...
.PHONY: wire
@@ -79,6 +85,10 @@ build: warning-noui-build check_go_env ##@Build Build only backend
go build -ldflags="-X github.com/navidrome/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/navidrome/navidrome/consts.gitTag=$(GIT_TAG)-SNAPSHOT" -tags=netgo
.PHONY: build
debug-build: warning-noui-build check_go_env ##@Build Build only backend (with remote debug on)
go build -gcflags="all=-N -l" -ldflags="-X github.com/navidrome/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/navidrome/navidrome/consts.gitTag=$(GIT_TAG)-SNAPSHOT" -tags=netgo
.PHONY: debug-build
buildjs: check_node_env ##@Build Build only frontend
@(cd ./ui && npm run build)
.PHONY: buildjs

View File

@@ -12,6 +12,7 @@ import (
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/playback"
"github.com/navidrome/navidrome/db"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/resources"
@@ -75,6 +76,10 @@ func runNavidrome() {
g.Go(startScheduler(ctx))
g.Go(schedulePeriodicScan(ctx))
if conf.Server.Jukebox.Enabled {
g.Go(startPlaybackServer(ctx))
}
if err := g.Wait(); err != nil && !errors.Is(err, interrupted) {
log.Error("Fatal error in Navidrome. Aborting", err)
}
@@ -146,6 +151,16 @@ func startScheduler(ctx context.Context) func() error {
}
}
func startPlaybackServer(ctx context.Context) func() error {
log.Info(ctx, "Starting playback server")
playbackInstance := playback.GetInstance()
return func() error {
return playbackInstance.Run(ctx)
}
}
// TODO: Implement some struct tags to map flags to viper
func init() {
cobra.OnInitialize(func() {

View File

@@ -40,7 +40,8 @@ func CreateNativeAPIRouter() *nativeapi.Router {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
share := core.NewShare(dataStore)
router := nativeapi.New(dataStore, share)
playlists := core.NewPlaylists(dataStore)
router := nativeapi.New(dataStore, share, playlists)
return router
}

View File

@@ -55,6 +55,7 @@ type configOptions struct {
IndexGroups string
SubsonicArtistParticipations bool
FFmpegPath string
MPVPath string
CoverArtPriority string
CoverJpegQuality int
ArtistArtPriority string
@@ -78,6 +79,7 @@ type configOptions struct {
ReverseProxyWhitelist string
Prometheus prometheusOptions
Scanner scannerOptions
Jukebox jukeboxOptions
Agents string
LastFM lastfmOptions
@@ -129,6 +131,14 @@ type prometheusOptions struct {
MetricsPath string
}
type AudioDeviceDefinition []string
type jukeboxOptions struct {
Enabled bool
Devices []AudioDeviceDefinition
Default string
}
var (
Server = &configOptions{}
hooks []func()
@@ -280,7 +290,7 @@ func init() {
viper.SetDefault("playlistspath", consts.DefaultPlaylistsPath)
viper.SetDefault("enabledownloads", true)
viper.SetDefault("enableexternalservices", true)
viper.SetDefault("enableMediaFileCoverArt", true)
viper.SetDefault("enablemediafilecoverart", true)
viper.SetDefault("autotranscodedownload", false)
viper.SetDefault("defaultdownsamplingformat", consts.DefaultDownsamplingFormat)
viper.SetDefault("searchfullstring", false)
@@ -313,6 +323,10 @@ func init() {
viper.SetDefault("prometheus.enabled", false)
viper.SetDefault("prometheus.metricspath", "/metrics")
viper.SetDefault("jukebox.enabled", false)
viper.SetDefault("jukebox.devices", []AudioDeviceDefinition{})
viper.SetDefault("jukebox.default", "")
viper.SetDefault("scanner.extractor", consts.DefaultScannerExtractor)
viper.SetDefault("scanner.genreseparators", ";/,")
viper.SetDefault("scanner.groupalbumreleases", false)

View File

@@ -257,7 +257,7 @@ func (l *lastfmAgent) NowPlaying(ctx context.Context, userId string, track *mode
track: track.Title,
album: track.Album,
trackNumber: track.TrackNumber,
mbid: track.MbzTrackID,
mbid: track.MbzRecordingID,
duration: int(track.Duration),
albumArtist: track.AlbumArtist,
})
@@ -283,7 +283,7 @@ func (l *lastfmAgent) Scrobble(ctx context.Context, userId string, s scrobbler.S
track: s.Title,
album: s.Album,
trackNumber: s.TrackNumber,
mbid: s.MbzTrackID,
mbid: s.MbzRecordingID,
duration: int(s.Duration),
albumArtist: s.AlbumArtist,
timestamp: s.TimeStamp,

View File

@@ -234,14 +234,14 @@ var _ = Describe("lastfmAgent", func() {
agent = lastFMConstructor(ds)
agent.client = client
track = &model.MediaFile{
ID: "123",
Title: "Track Title",
Album: "Track Album",
Artist: "Track Artist",
AlbumArtist: "Track AlbumArtist",
TrackNumber: 1,
Duration: 180,
MbzTrackID: "mbz-123",
ID: "123",
Title: "Track Title",
Album: "Track Album",
Artist: "Track Artist",
AlbumArtist: "Track AlbumArtist",
TrackNumber: 1,
Duration: 180,
MbzRecordingID: "mbz-123",
}
})
@@ -262,7 +262,7 @@ var _ = Describe("lastfmAgent", func() {
Expect(sentParams.Get("albumArtist")).To(Equal(track.AlbumArtist))
Expect(sentParams.Get("trackNumber")).To(Equal(strconv.Itoa(track.TrackNumber)))
Expect(sentParams.Get("duration")).To(Equal(strconv.FormatFloat(float64(track.Duration), 'G', -1, 32)))
Expect(sentParams.Get("mbid")).To(Equal(track.MbzTrackID))
Expect(sentParams.Get("mbid")).To(Equal(track.MbzRecordingID))
})
It("returns ErrNotAuthorized if user is not linked", func() {
@@ -289,7 +289,7 @@ var _ = Describe("lastfmAgent", func() {
Expect(sentParams.Get("albumArtist")).To(Equal(track.AlbumArtist))
Expect(sentParams.Get("trackNumber")).To(Equal(strconv.Itoa(track.TrackNumber)))
Expect(sentParams.Get("duration")).To(Equal(strconv.FormatFloat(float64(track.Duration), 'G', -1, 32)))
Expect(sentParams.Get("mbid")).To(Equal(track.MbzTrackID))
Expect(sentParams.Get("mbid")).To(Equal(track.MbzRecordingID))
Expect(sentParams.Get("timestamp")).To(Equal(strconv.FormatInt(ts.Unix(), 10)))
})

View File

@@ -55,8 +55,9 @@ func (l *listenBrainzAgent) formatListen(track *model.MediaFile) listenInfo {
SubmissionClientVersion: consts.Version,
TrackNumber: track.TrackNumber,
ArtistMbzIDs: []string{track.MbzArtistID},
TrackMbzID: track.MbzTrackID,
RecordingMbzID: track.MbzRecordingID,
ReleaseMbID: track.MbzAlbumID,
DurationMs: int(track.Duration * 1000),
},
},
}

View File

@@ -32,14 +32,15 @@ var _ = Describe("listenBrainzAgent", func() {
agent = listenBrainzConstructor(ds)
agent.client = newClient("http://localhost:8080", httpClient)
track = &model.MediaFile{
ID: "123",
Title: "Track Title",
Album: "Track Album",
Artist: "Track Artist",
TrackNumber: 1,
MbzTrackID: "mbz-123",
MbzAlbumID: "mbz-456",
MbzArtistID: "mbz-789",
ID: "123",
Title: "Track Title",
Album: "Track Album",
Artist: "Track Artist",
TrackNumber: 1,
MbzRecordingID: "mbz-123",
MbzAlbumID: "mbz-456",
MbzArtistID: "mbz-789",
Duration: 142.2,
}
})
@@ -60,11 +61,12 @@ var _ = Describe("listenBrainzAgent", func() {
"SubmissionClient": Equal(consts.AppName),
"SubmissionClientVersion": Equal(consts.Version),
"TrackNumber": Equal(track.TrackNumber),
"TrackMbzID": Equal(track.MbzTrackID),
"RecordingMbzID": Equal(track.MbzRecordingID),
"ReleaseMbID": Equal(track.MbzAlbumID),
"ArtistMbzIDs": MatchAllElements(idArtistId, Elements{
"mbz-789": Equal(track.MbzArtistID),
}),
"DurationMs": Equal(142200),
}),
}),
}))

View File

@@ -76,9 +76,10 @@ type additionalInfo struct {
SubmissionClient string `json:"submission_client,omitempty"`
SubmissionClientVersion string `json:"submission_client_version,omitempty"`
TrackNumber int `json:"tracknumber,omitempty"`
TrackMbzID string `json:"track_mbid,omitempty"`
RecordingMbzID string `json:"recording_mbid,omitempty"`
ArtistMbzIDs []string `json:"artist_mbids,omitempty"`
ReleaseMbID string `json:"release_mbid,omitempty"`
DurationMs int `json:"duration_ms,omitempty"`
}
func (c *client) validateToken(ctx context.Context, apiKey string) (*listenBrainzResponse, error) {

View File

@@ -74,10 +74,11 @@ var _ = Describe("client", func() {
TrackName: "Track Title",
ReleaseName: "Track Album",
AdditionalInfo: additionalInfo{
TrackNumber: 1,
TrackMbzID: "mbz-123",
ArtistMbzIDs: []string{"mbz-789"},
ReleaseMbID: "mbz-456",
TrackNumber: 1,
RecordingMbzID: "mbz-123",
ArtistMbzIDs: []string{"mbz-789"},
ReleaseMbID: "mbz-456",
DurationMs: 142200,
},
},
}

View File

@@ -399,7 +399,7 @@ func (e *externalMetadata) getMatchingTopSongs(ctx context.Context, agent agents
func (e *externalMetadata) findMatchingTrack(ctx context.Context, mbid string, artistID, title string) (*model.MediaFile, error) {
if mbid != "" {
mfs, err := e.ds.MediaFile(ctx).GetAll(model.QueryOptions{
Filters: squirrel.Eq{"mbz_track_id": mbid},
Filters: squirrel.Eq{"mbz_recording_id": mbid},
})
if err == nil && len(mfs) > 0 {
return &mfs[0], nil
@@ -414,7 +414,7 @@ func (e *externalMetadata) findMatchingTrack(ctx context.Context, mbid string, a
},
squirrel.Like{"order_title": strings.TrimSpace(sanitize.Accents(title))},
},
Sort: "starred desc, rating desc, year asc",
Sort: "starred desc, rating desc, year asc, compilation asc ",
Max: 1,
})
if err != nil || len(mfs) == 0 {

View File

@@ -18,6 +18,8 @@ import (
type FFmpeg interface {
Transcode(ctx context.Context, command, path string, maxBitRate int) (io.ReadCloser, error)
ExtractImage(ctx context.Context, path string) (io.ReadCloser, error)
ConvertToWAV(ctx context.Context, path string) (io.ReadCloser, error)
ConvertToFLAC(ctx context.Context, path string) (io.ReadCloser, error)
Probe(ctx context.Context, files []string) (string, error)
CmdPath() (string, error)
}
@@ -29,6 +31,8 @@ func New() FFmpeg {
const (
extractImageCmd = "ffmpeg -i %s -an -vcodec copy -f image2pipe -"
probeCmd = "ffmpeg %s -f ffmetadata"
createWavCmd = "ffmpeg -i %s -c:a pcm_s16le -f wav -"
createFLACCmd = "ffmpeg -i %s -f flac -"
)
type ffmpeg struct{}
@@ -49,6 +53,16 @@ func (e *ffmpeg) ExtractImage(ctx context.Context, path string) (io.ReadCloser,
return e.start(ctx, args)
}
func (e *ffmpeg) ConvertToWAV(ctx context.Context, path string) (io.ReadCloser, error) {
args := createFFmpegCommand(createWavCmd, path, 0)
return e.start(ctx, args)
}
func (e *ffmpeg) ConvertToFLAC(ctx context.Context, path string) (io.ReadCloser, error) {
args := createFFmpegCommand(createFLACCmd, path, 0)
return e.start(ctx, args)
}
func (e *ffmpeg) Probe(ctx context.Context, files []string) (string, error) {
if _, err := ffmpegCmd(); err != nil {
return "", err

View File

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

View File

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

285
core/playback/device.go Normal file
View File

@@ -0,0 +1,285 @@
package playback
import (
"context"
"fmt"
"github.com/navidrome/navidrome/core/playback/mpv"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
)
type Track interface {
IsPlaying() bool
SetVolume(value float32) // Used to control the playback volume. A float value between 0.0 and 1.0.
Pause()
Unpause()
Position() int
SetPosition(offset int) error
Close()
}
type PlaybackDevice struct {
ParentPlaybackServer PlaybackServer
Default bool
User string
Name string
DeviceName string
PlaybackQueue *Queue
Gain float32
PlaybackDone chan bool
ActiveTrack Track
TrackSwitcherStarted bool
}
type DeviceStatus struct {
CurrentIndex int
Playing bool
Gain float32
Position int
}
const DefaultGain float32 = 1.0
var EmptyStatus = DeviceStatus{CurrentIndex: -1, Playing: false, Gain: DefaultGain, Position: 0}
func (pd *PlaybackDevice) getStatus() DeviceStatus {
pos := 0
if pd.ActiveTrack != nil {
pos = pd.ActiveTrack.Position()
}
return DeviceStatus{
CurrentIndex: pd.PlaybackQueue.Index,
Playing: pd.isPlaying(),
Gain: pd.Gain,
Position: pos,
}
}
// NewPlaybackDevice creates a new playback device which implements all the basic Jukebox mode commands defined here:
// http://www.subsonic.org/pages/api.jsp#jukeboxControl
// Starts the trackSwitcher goroutine for the device.
func NewPlaybackDevice(playbackServer PlaybackServer, name string, deviceName string) *PlaybackDevice {
return &PlaybackDevice{
ParentPlaybackServer: playbackServer,
User: "",
Name: name,
DeviceName: deviceName,
Gain: DefaultGain,
PlaybackQueue: NewQueue(),
PlaybackDone: make(chan bool),
TrackSwitcherStarted: false,
}
}
func (pd *PlaybackDevice) String() string {
return fmt.Sprintf("Name: %s, Gain: %.4f, Loaded track: %s", pd.Name, pd.Gain, pd.ActiveTrack)
}
func (pd *PlaybackDevice) Get(ctx context.Context) (model.MediaFiles, DeviceStatus, error) {
log.Debug(ctx, "processing Get action")
return pd.PlaybackQueue.Get(), pd.getStatus(), nil
}
func (pd *PlaybackDevice) Status(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, fmt.Sprintf("processing Status action on: %s, queue: %s", pd, pd.PlaybackQueue))
return pd.getStatus(), nil
}
// set is similar to a clear followed by a add, but will not change the currently playing track.
func (pd *PlaybackDevice) Set(ctx context.Context, ids []string) (DeviceStatus, error) {
_, err := pd.Clear(ctx)
if err != nil {
log.Error(ctx, "error setting tracks", ids)
return pd.getStatus(), err
}
return pd.Add(ctx, ids)
}
func (pd *PlaybackDevice) Start(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, "processing Start action")
if !pd.TrackSwitcherStarted {
log.Info(ctx, "Starting trackSwitcher goroutine")
// Start one trackSwitcher goroutine with each device
go func() {
pd.trackSwitcherGoroutine()
}()
pd.TrackSwitcherStarted = true
}
if pd.ActiveTrack != nil {
if pd.isPlaying() {
log.Debug("trying to start an already playing track")
} else {
pd.ActiveTrack.Unpause()
}
} else {
if !pd.PlaybackQueue.IsEmpty() {
err := pd.switchActiveTrackByIndex(pd.PlaybackQueue.Index)
if err != nil {
return pd.getStatus(), err
}
pd.ActiveTrack.Unpause()
}
}
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) Stop(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, "processing Stop action")
if pd.ActiveTrack != nil {
pd.ActiveTrack.Pause()
}
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) Skip(ctx context.Context, index int, offset int) (DeviceStatus, error) {
log.Debug(ctx, "processing Skip action", "index", index, "offset", offset)
wasPlaying := pd.isPlaying()
if pd.ActiveTrack != nil && wasPlaying {
pd.ActiveTrack.Pause()
}
if index != pd.PlaybackQueue.Index && pd.ActiveTrack != nil {
pd.ActiveTrack.Close()
pd.ActiveTrack = nil
}
if pd.ActiveTrack == nil {
err := pd.switchActiveTrackByIndex(index)
if err != nil {
return pd.getStatus(), err
}
}
err := pd.ActiveTrack.SetPosition(offset)
if err != nil {
log.Error(ctx, "error setting position", err)
return pd.getStatus(), err
}
if wasPlaying {
_, err = pd.Start(ctx)
if err != nil {
log.Error(ctx, "error starting new track after skipping")
return pd.getStatus(), err
}
}
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) Add(ctx context.Context, ids []string) (DeviceStatus, error) {
log.Debug(ctx, "processing Add action")
items := model.MediaFiles{}
for _, id := range ids {
mf, err := pd.ParentPlaybackServer.GetMediaFile(id)
if err != nil {
return DeviceStatus{}, err
}
log.Debug(ctx, "Found mediafile: "+mf.Path)
items = append(items, *mf)
}
pd.PlaybackQueue.Add(items)
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) Clear(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, fmt.Sprintf("processing Clear action on: %s", pd))
if pd.ActiveTrack != nil {
pd.ActiveTrack.Pause()
pd.ActiveTrack.Close()
pd.ActiveTrack = nil
}
pd.PlaybackQueue.Clear()
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) Remove(ctx context.Context, index int) (DeviceStatus, error) {
log.Debug(ctx, "processing Remove action")
// pausing if attempting to remove running track
if pd.isPlaying() && pd.PlaybackQueue.Index == index {
_, err := pd.Stop(ctx)
if err != nil {
log.Error(ctx, "error stopping running track")
return pd.getStatus(), err
}
}
if index > -1 && index < pd.PlaybackQueue.Size() {
pd.PlaybackQueue.Remove(index)
} else {
log.Error(ctx, "Index to remove out of range: "+fmt.Sprint(index))
}
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) Shuffle(ctx context.Context) (DeviceStatus, error) {
log.Debug(ctx, "processing Shuffle action")
if pd.PlaybackQueue.Size() > 1 {
pd.PlaybackQueue.Shuffle()
}
return pd.getStatus(), nil
}
// Used to control the playback volume. A float value between 0.0 and 1.0.
func (pd *PlaybackDevice) SetGain(ctx context.Context, gain float32) (DeviceStatus, error) {
log.Debug(ctx, fmt.Sprintf("processing SetGain action. Actual gain: %f, gain to set: %f", pd.Gain, gain))
if pd.ActiveTrack != nil {
pd.ActiveTrack.SetVolume(gain)
}
pd.Gain = gain
return pd.getStatus(), nil
}
func (pd *PlaybackDevice) isPlaying() bool {
return pd.ActiveTrack != nil && pd.ActiveTrack.IsPlaying()
}
func (pd *PlaybackDevice) trackSwitcherGoroutine() {
log.Info("Starting trackSwitcher goroutine")
for {
<-pd.PlaybackDone
log.Info("track switching detected")
if pd.ActiveTrack != nil {
pd.ActiveTrack.Close()
pd.ActiveTrack = nil
}
if !pd.PlaybackQueue.IsAtLastElement() {
pd.PlaybackQueue.IncreaseIndex()
log.Debug("Switching to next song", "queue", pd.PlaybackQueue.String())
err := pd.switchActiveTrackByIndex(pd.PlaybackQueue.Index)
if err != nil {
log.Error("error switching track", "error", err)
}
pd.ActiveTrack.Unpause()
} else {
log.Debug("There is no song left in the playlist. Finish.")
}
}
}
func (pd *PlaybackDevice) switchActiveTrackByIndex(index int) error {
pd.PlaybackQueue.SetIndex(index)
currentTrack := pd.PlaybackQueue.Current()
if currentTrack == nil {
return fmt.Errorf("could not get current track")
}
track, err := mpv.NewTrack(pd.PlaybackDone, pd.DeviceName, *currentTrack)
if err != nil {
return err
}
pd.ActiveTrack = track
return nil
}

142
core/playback/mpv/mpv.go Normal file
View File

@@ -0,0 +1,142 @@
package mpv
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
)
// mpv --no-audio-display --pause 'Jack Johnson/On And On/01 Times Like These.m4a' --input-ipc-server=/tmp/gonzo.socket
const (
mpvComdTemplate = "mpv --audio-device=%d --no-audio-display --pause %f --input-ipc-server=%s"
)
func start(args []string) (Executor, error) {
log.Debug("Executing mpv command", "cmd", args)
j := Executor{args: args}
j.PipeReader, j.out = io.Pipe()
err := j.start()
if err != nil {
return Executor{}, err
}
go j.wait()
return j, nil
}
func (j *Executor) Cancel() error {
if j.cmd != nil {
return j.cmd.Cancel()
}
return fmt.Errorf("there is non command to cancel")
}
type Executor struct {
*io.PipeReader
out *io.PipeWriter
args []string
cmd *exec.Cmd
ctx context.Context
}
func (j *Executor) start() error {
ctx := context.Background()
j.ctx = ctx
cmd := exec.CommandContext(ctx, j.args[0], j.args[1:]...) // #nosec
cmd.Stdout = j.out
if log.CurrentLevel() >= log.LevelTrace {
cmd.Stderr = os.Stderr
} else {
cmd.Stderr = io.Discard
}
j.cmd = cmd
if err := cmd.Start(); err != nil {
return fmt.Errorf("starting cmd: %w", err)
}
return nil
}
func (j *Executor) wait() {
if err := j.cmd.Wait(); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
_ = j.out.CloseWithError(fmt.Errorf("%s exited with non-zero status code: %d", j.args[0], exitErr.ExitCode()))
} else {
_ = j.out.CloseWithError(fmt.Errorf("waiting %s cmd: %w", j.args[0], err))
}
return
}
_ = j.out.Close()
}
// Path will always be an absolute path
func createMPVCommand(cmd, deviceName string, filename string, socketName string) []string {
split := strings.Split(fixCmd(cmd), " ")
for i, s := range split {
s = strings.ReplaceAll(s, "%d", deviceName)
s = strings.ReplaceAll(s, "%f", filename)
s = strings.ReplaceAll(s, "%s", socketName)
split[i] = s
}
return split
}
func fixCmd(cmd string) string {
split := strings.Split(cmd, " ")
var result []string
cmdPath, _ := mpvCommand()
for _, s := range split {
if s == "mpv" || s == "mpv.exe" {
result = append(result, cmdPath)
} else {
result = append(result, s)
}
}
return strings.Join(result, " ")
}
// This is a 1:1 copy of the stuff in ffmpeg.go, need to be unified.
func mpvCommand() (string, error) {
mpvOnce.Do(func() {
if conf.Server.MPVPath != "" {
mpvPath = conf.Server.MPVPath
mpvPath, mpvErr = exec.LookPath(mpvPath)
} else {
mpvPath, mpvErr = exec.LookPath("mpv")
if errors.Is(mpvErr, exec.ErrDot) {
log.Trace("mpv found in current folder '.'")
mpvPath, mpvErr = exec.LookPath("./mpv")
}
}
if mpvErr == nil {
log.Info("Found mpv", "path", mpvPath)
return
}
})
return mpvPath, mpvErr
}
var (
mpvOnce sync.Once
mpvPath string
mpvErr error
)
func TempFileName(prefix, suffix string) string {
randBytes := make([]byte, 16)
// we can savely ignore the return value since we're loading into a precreated, fixedsized buffer
_, _ = rand.Read(randBytes)
return filepath.Join(os.TempDir(), prefix+hex.EncodeToString(randBytes)+suffix)
}

223
core/playback/mpv/track.go Normal file
View File

@@ -0,0 +1,223 @@
package mpv
// Audio-playback using mpv media-server. See mpv.io
// https://github.com/dexterlb/mpvipc
// https://mpv.io/manual/master/#json-ipc
// https://mpv.io/manual/master/#properties
import (
"fmt"
"os"
"time"
"github.com/DexterLB/mpvipc"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
)
type MpvTrack struct {
MediaFile model.MediaFile
PlaybackDone chan bool
Conn *mpvipc.Connection
IPCSocketName string
Exe *Executor
CloseCalled bool
}
func NewTrack(playbackDoneChannel chan bool, deviceName string, mf model.MediaFile) (*MpvTrack, error) {
log.Debug("loading track", "trackname", mf.Path, "mediatype", mf.ContentType())
if _, err := mpvCommand(); err != nil {
return nil, err
}
tmpSocketName := TempFileName("mpv-ctrl-", ".socket")
args := createMPVCommand(mpvComdTemplate, deviceName, mf.Path, tmpSocketName)
exe, err := start(args)
if err != nil {
log.Error("error starting mpv process", "error", err)
return nil, err
}
// wait for socket to show up
err = waitForFile(tmpSocketName, 3*time.Second, 100*time.Millisecond)
if err != nil {
log.Error("error or timeout waiting for control socket", "socketname", tmpSocketName, "error", err)
return nil, err
}
conn := mpvipc.NewConnection(tmpSocketName)
err = conn.Open()
if err != nil {
log.Error("error opening new connection", "error", err)
return nil, err
}
theTrack := &MpvTrack{MediaFile: mf, PlaybackDone: playbackDoneChannel, Conn: conn, IPCSocketName: tmpSocketName, Exe: &exe, CloseCalled: false}
go func() {
conn.WaitUntilClosed()
log.Info("Hitting end-of-stream, signalling on channel")
if !theTrack.CloseCalled {
playbackDoneChannel <- true
}
}()
return theTrack, nil
}
func (t *MpvTrack) String() string {
return fmt.Sprintf("Name: %s, Socket: %s", t.MediaFile.Path, t.IPCSocketName)
}
// Used to control the playback volume. A float value between 0.0 and 1.0.
func (t *MpvTrack) SetVolume(value float32) {
// mpv's volume as described in the --volume parameter:
// Set the startup volume. 0 means silence, 100 means no volume reduction or amplification.
// Negative values can be passed for compatibility, but are treated as 0.
log.Debug("request for gain", "gain", value)
vol := int(value * 100)
err := t.Conn.Set("volume", vol)
if err != nil {
log.Error(err)
}
log.Debug("set volume", "volume", vol)
}
func (t *MpvTrack) Unpause() {
err := t.Conn.Set("pause", false)
if err != nil {
log.Error(err)
}
log.Info("unpaused track")
}
func (t *MpvTrack) Pause() {
err := t.Conn.Set("pause", true)
if err != nil {
log.Error(err)
}
log.Info("paused track")
}
func (t *MpvTrack) Close() {
log.Debug("closing resources")
t.CloseCalled = true
// trying to shutdown mpv process using socket
if t.isSocketfilePresent() {
log.Debug("sending shutdown command")
_, err := t.Conn.Call("quit")
if err != nil {
log.Error("error sending quit command to mpv-ipc socket", "error", err)
if t.Exe != nil {
log.Debug("cancelling executor")
err = t.Exe.Cancel()
if err != nil {
log.Error("error canceling executor")
}
}
}
}
if t.isSocketfilePresent() {
log.Debug("Removing socketfile", "socketfile", t.IPCSocketName)
err := os.Remove(t.IPCSocketName)
if err != nil {
log.Error("error cleaning up socketfile: ", t.IPCSocketName)
}
}
}
func (t *MpvTrack) isSocketfilePresent() bool {
if len(t.IPCSocketName) < 1 {
return false
}
fileInfo, err := os.Stat(t.IPCSocketName)
return err == nil && fileInfo != nil && !fileInfo.IsDir()
}
// Position returns the playback position in seconds
// every now and then the mpv IPC interface returns "mpv error: property unavailable"
// in this case we have to retry
func (t *MpvTrack) Position() int {
retryCount := 0
for {
position, err := t.Conn.Get("time-pos")
if err != nil && err.Error() == "mpv error: property unavailable" {
log.Debug("got the mpv error: property unavailable error, retry ...")
retryCount += 1
if retryCount > 5 {
return 0
}
break
}
if err != nil {
log.Error("error getting position in track", "error", err)
return 0
}
pos, ok := position.(float64)
if !ok {
log.Error("could not cast position from mpv into float64")
return 0
} else {
return int(pos)
}
}
return 0
}
func (t *MpvTrack) SetPosition(offset int) error {
pos := t.Position()
if pos == offset {
log.Debug("no position difference, skipping operation")
return nil
}
err := t.Conn.Set("time-pos", float64(offset))
if err != nil {
log.Error("could not set the position in track", "offset", offset, "error", err)
return err
}
log.Info("set position", "offset", offset)
return nil
}
func (t *MpvTrack) IsPlaying() bool {
pausing, err := t.Conn.Get("pause")
if err != nil {
log.Error("problem getting paused status", "error", err)
return false
}
pause, ok := pausing.(bool)
if !ok {
log.Error("could not cast pausing to boolean")
return false
}
return !pause
}
func waitForFile(path string, timeout time.Duration, pause time.Duration) error {
start := time.Now()
end := start.Add(timeout)
var retries int = 0
for {
fileInfo, err := os.Stat(path)
if err == nil && fileInfo != nil && !fileInfo.IsDir() {
log.Debug("file found", "retries", retries, "waittime", time.Since(start).Microseconds())
return nil
}
if time.Now().After(end) {
return fmt.Errorf("timeout reached: %s", timeout)
}
time.Sleep(pause)
retries += 1
}
}

View File

@@ -0,0 +1,17 @@
package playback
import (
"testing"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestPlayback(t *testing.T) {
tests.Init(t, false)
log.SetLevel(log.LevelFatal)
RegisterFailHandler(Fail)
RunSpecs(t, "Playback Suite")
}

View File

@@ -0,0 +1,135 @@
// Package playback implements audio playback using PlaybackDevices. It is used to implement the Jukebox mode in turn.
// It makes use of the BEEP library to do the playback. Major parts are:
// - decoder which includes decoding and transcoding of various audio file formats
// - device implementing the basic functions to work with audio devices like set, play, stop, skip, ...
// - queue a simple playlist
package playback
import (
"context"
"fmt"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/db"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/persistence"
"github.com/navidrome/navidrome/utils/singleton"
)
type PlaybackServer interface {
Run(ctx context.Context) error
GetDeviceForUser(user string) (*PlaybackDevice, error)
GetMediaFile(id string) (*model.MediaFile, error)
GetCtx() *context.Context
}
type playbackServer struct {
ctx *context.Context
datastore model.DataStore
playbackDevices []PlaybackDevice
}
// GetInstance returns the playback-server singleton
func GetInstance() PlaybackServer {
return singleton.GetInstance(func() *playbackServer {
return &playbackServer{}
})
}
// Run starts the playback server which serves request until canceled using the given context
func (ps *playbackServer) Run(ctx context.Context) error {
ps.datastore = persistence.New(db.Db())
devices, err := ps.initDeviceStatus(conf.Server.Jukebox.Devices, conf.Server.Jukebox.Default)
ps.playbackDevices = devices
if err != nil {
return err
}
log.Info(ctx, fmt.Sprintf("%d audio devices found", len(devices)))
defaultDevice, _ := ps.getDefaultDevice()
log.Info(ctx, "Using audio device: "+defaultDevice.DeviceName)
ps.ctx = &ctx
<-ctx.Done()
return nil
}
// GetCtx produces the context this server was started with. Used for data-retrieval and cancellation
func (ps *playbackServer) GetCtx() *context.Context {
return ps.ctx
}
func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition, defaultDevice string) ([]PlaybackDevice, error) {
pbDevices := make([]PlaybackDevice, max(1, len(devices)))
defaultDeviceFound := false
if defaultDevice == "" {
// if there are no devices given and no default device, we create a sythetic device named "auto"
if len(devices) == 0 {
pbDevices[0] = *NewPlaybackDevice(ps, "auto", "auto")
}
// if there is but only one entry and no default given, just use that.
if len(devices) == 1 {
if len(devices[0]) != 2 {
return []PlaybackDevice{}, fmt.Errorf("audio device definition ought to contain 2 fields, found: %d ", len(devices[0]))
}
pbDevices[0] = *NewPlaybackDevice(ps, devices[0][0], devices[0][1])
}
if len(devices) > 1 {
return []PlaybackDevice{}, fmt.Errorf("number of audio device found is %d, but no default device defined. Set Jukebox.Default", len(devices))
}
pbDevices[0].Default = true
return pbDevices, nil
}
for idx, audioDevice := range devices {
if len(audioDevice) != 2 {
return []PlaybackDevice{}, fmt.Errorf("audio device definition ought to contain 2 fields, found: %d ", len(audioDevice))
}
pbDevices[idx] = *NewPlaybackDevice(ps, audioDevice[0], audioDevice[1])
if audioDevice[0] == defaultDevice {
pbDevices[idx].Default = true
defaultDeviceFound = true
}
}
if !defaultDeviceFound {
return []PlaybackDevice{}, fmt.Errorf("default device name not found: %s ", defaultDevice)
}
return pbDevices, nil
}
func (ps *playbackServer) getDefaultDevice() (*PlaybackDevice, error) {
for idx, audioDevice := range ps.playbackDevices {
if audioDevice.Default {
return &ps.playbackDevices[idx], nil
}
}
return &PlaybackDevice{}, fmt.Errorf("no default device found")
}
// GetMediaFile retrieves the MediaFile given by the id parameter
func (ps *playbackServer) GetMediaFile(id string) (*model.MediaFile, error) {
return ps.datastore.MediaFile(*ps.ctx).Get(id)
}
// GetDeviceForUser returns the audio playback device for the given user. As of now this is but only the default device.
func (ps *playbackServer) GetDeviceForUser(user string) (*PlaybackDevice, error) {
log.Debug("processing GetDevice")
// README: here we might plug-in the user-device mapping one fine day
device, err := ps.getDefaultDevice()
if err != nil {
return &PlaybackDevice{}, err
}
device.User = user
return device, nil
}

150
core/playback/queue.go Normal file
View File

@@ -0,0 +1,150 @@
package playback
import (
"fmt"
"math/rand"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
)
type Queue struct {
Index int
Items model.MediaFiles
}
func NewQueue() *Queue {
return &Queue{
Index: -1,
Items: model.MediaFiles{},
}
}
func (pd *Queue) String() string {
filenames := ""
for idx, item := range pd.Items {
filenames += fmt.Sprint(idx) + ":" + item.Path + " "
}
return fmt.Sprintf("#Items: %d, idx: %d, files: %s", len(pd.Items), pd.Index, filenames)
}
// returns the current mediafile or nil
func (pd *Queue) Current() *model.MediaFile {
if pd.Index == -1 {
return nil
}
if pd.Index >= len(pd.Items) {
log.Error("internal error: current song index out of bounds", "idx", pd.Index, "length", len(pd.Items))
return nil
}
return &pd.Items[pd.Index]
}
// returns the whole queue
func (pd *Queue) Get() model.MediaFiles {
return pd.Items
}
func (pd *Queue) Size() int {
return len(pd.Items)
}
func (pd *Queue) IsEmpty() bool {
return len(pd.Items) < 1
}
// set is similar to a clear followed by a add, but will not change the currently playing track.
func (pd *Queue) Set(items model.MediaFiles) {
pd.Clear()
pd.Items = append(pd.Items, items...)
}
// adding mediafiles to the queue
func (pd *Queue) Add(items model.MediaFiles) {
pd.Items = append(pd.Items, items...)
if pd.Index == -1 && len(pd.Items) > 0 {
pd.Index = 0
}
}
// empties whole queue
func (pd *Queue) Clear() {
pd.Index = -1
pd.Items = nil
}
// idx Zero-based index of the song to skip to or remove.
func (pd *Queue) Remove(idx int) {
current := pd.Current()
backupID := ""
if current != nil {
backupID = current.ID
}
pd.Items = append(pd.Items[:idx], pd.Items[idx+1:]...)
var err error
pd.Index, err = pd.getMediaFileIndexByID(backupID)
if err != nil {
// we seem to have deleted the current id, setting to default:
pd.Index = -1
}
}
func (pd *Queue) Shuffle() {
current := pd.Current()
backupID := ""
if current != nil {
backupID = current.ID
}
rand.Shuffle(len(pd.Items), func(i, j int) { pd.Items[i], pd.Items[j] = pd.Items[j], pd.Items[i] })
var err error
pd.Index, err = pd.getMediaFileIndexByID(backupID)
if err != nil {
log.Error("Could not find ID while shuffling: " + backupID)
}
}
func (pd *Queue) getMediaFileIndexByID(id string) (int, error) {
for idx, item := range pd.Items {
if item.ID == id {
return idx, nil
}
}
return -1, fmt.Errorf("ID not found in playlist: " + id)
}
// Sets the index to a new, valid value inside the Items. Values lower than zero are going to be zero,
// values above will be limited by number of items.
func (pd *Queue) SetIndex(idx int) {
pd.Index = max(0, min(idx, len(pd.Items)-1))
}
// Are we at the last track?
func (pd *Queue) IsAtLastElement() bool {
return (pd.Index + 1) >= len(pd.Items)
}
// Goto next index
func (pd *Queue) IncreaseIndex() {
if !pd.IsAtLastElement() {
pd.SetIndex(pd.Index + 1)
}
}
func max(x, y int) int {
if x < y {
return y
}
return x
}
func min(x, y int) int {
if x > y {
return y
}
return x
}

121
core/playback/queue_test.go Normal file
View File

@@ -0,0 +1,121 @@
package playback
import (
"github.com/navidrome/navidrome/model"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Queues", func() {
var queue *Queue
BeforeEach(func() {
queue = NewQueue()
})
Describe("use empty queue", func() {
It("is empty", func() {
Expect(queue.Items).To(BeEmpty())
Expect(queue.Index).To(Equal(-1))
})
})
Describe("Operate on small queue", func() {
BeforeEach(func() {
mfs := model.MediaFiles{
{
ID: "1", Artist: "Queen", Compilation: false, Path: "/music1/hammer.mp3",
},
{
ID: "2", Artist: "Vinyard Rose", Compilation: false, Path: "/music1/cassidy.mp3",
},
}
queue.Add(mfs)
})
It("contains the preloaded data", func() {
Expect(queue.Get).ToNot(BeNil())
Expect(queue.Size()).To(Equal(2))
})
It("could read data by ID", func() {
idx, err := queue.getMediaFileIndexByID("1")
Expect(err).ToNot(HaveOccurred())
Expect(idx).ToNot(BeNil())
Expect(idx).To(Equal(0))
queue.SetIndex(idx)
mf := queue.Current()
Expect(mf).ToNot(BeNil())
Expect(mf.ID).To(Equal("1"))
Expect(mf.Artist).To(Equal("Queen"))
Expect(mf.Path).To(Equal("/music1/hammer.mp3"))
})
})
Describe("Read/Write operations", func() {
BeforeEach(func() {
mfs := model.MediaFiles{
{
ID: "1", Artist: "Queen", Compilation: false, Path: "/music1/hammer.mp3",
},
{
ID: "2", Artist: "Vinyard Rose", Compilation: false, Path: "/music1/cassidy.mp3",
},
{
ID: "3", Artist: "Pink Floyd", Compilation: false, Path: "/music1/time.mp3",
},
{
ID: "4", Artist: "Mike Oldfield", Compilation: false, Path: "/music1/moonlight-shadow.mp3",
},
{
ID: "5", Artist: "Red Hot Chili Peppers", Compilation: false, Path: "/music1/californication.mp3",
},
}
queue.Add(mfs)
})
It("contains the preloaded data", func() {
Expect(queue.Get).ToNot(BeNil())
Expect(queue.Size()).To(Equal(5))
})
It("could read data by ID", func() {
idx, err := queue.getMediaFileIndexByID("5")
Expect(err).ToNot(HaveOccurred())
Expect(idx).ToNot(BeNil())
Expect(idx).To(Equal(4))
queue.SetIndex(idx)
mf := queue.Current()
Expect(mf).ToNot(BeNil())
Expect(mf.ID).To(Equal("5"))
Expect(mf.Artist).To(Equal("Red Hot Chili Peppers"))
Expect(mf.Path).To(Equal("/music1/californication.mp3"))
})
It("could shuffle the data correctly", func() {
queue.Shuffle()
Expect(queue.Size()).To(Equal(5))
})
It("could remove entries correctly", func() {
queue.Remove(0)
Expect(queue.Size()).To(Equal(4))
queue.Remove(3)
Expect(queue.Size()).To(Equal(3))
})
It("clear the whole thing on request", func() {
Expect(queue.Size()).To(Equal(5))
queue.Clear()
Expect(queue.Size()).To(Equal(0))
})
})
})

View File

@@ -23,6 +23,7 @@ import (
type Playlists interface {
ImportFile(ctx context.Context, dir string, fname string) (*model.Playlist, error)
Update(ctx context.Context, playlistID string, name *string, comment *string, public *bool, idsToAdd []string, idxToRemove []int) error
ImportM3U(ctx context.Context, reader io.Reader) (*model.Playlist, error)
}
type playlists struct {
@@ -47,6 +48,26 @@ func (s *playlists) ImportFile(ctx context.Context, dir string, fname string) (*
return pls, err
}
func (s *playlists) ImportM3U(ctx context.Context, reader io.Reader) (*model.Playlist, error) {
owner, _ := request.UserFrom(ctx)
pls := &model.Playlist{
OwnerID: owner.ID,
Public: false,
Sync: true,
}
pls, err := s.parseM3U(ctx, pls, "", reader)
if err != nil {
log.Error(ctx, "Error parsing playlist", err)
return nil, err
}
err = s.ds.Playlist(ctx).Put(pls)
if err != nil {
log.Error(ctx, "Error saving playlist", err)
return nil, err
}
return pls, nil
}
func (s *playlists) parsePlaylist(ctx context.Context, playlistFile string, baseDir string) (*model.Playlist, error) {
pls, err := s.newSyncedPlaylist(baseDir, playlistFile)
if err != nil {
@@ -107,31 +128,40 @@ func (s *playlists) parseNSP(ctx context.Context, pls *model.Playlist, file io.R
return pls, nil
}
func (s *playlists) parseM3U(ctx context.Context, pls *model.Playlist, baseDir string, file io.Reader) (*model.Playlist, error) {
func (s *playlists) parseM3U(ctx context.Context, pls *model.Playlist, baseDir string, reader io.Reader) (*model.Playlist, error) {
mediaFileRepository := s.ds.MediaFile(ctx)
scanner := bufio.NewScanner(file)
scanner := bufio.NewScanner(reader)
scanner.Split(scanLines)
var mfs model.MediaFiles
for scanner.Scan() {
path := strings.TrimSpace(scanner.Text())
// Skip empty lines and extended info
if path == "" || strings.HasPrefix(path, "#") {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "#PLAYLIST:") {
if split := strings.Split(line, ":"); len(split) >= 2 {
pls.Name = split[1]
}
continue
}
if strings.HasPrefix(path, "file://") {
path = strings.TrimPrefix(path, "file://")
path, _ = url.QueryUnescape(path)
// Skip empty lines and extended info
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if !filepath.IsAbs(path) {
path = filepath.Join(baseDir, path)
if strings.HasPrefix(line, "file://") {
line = strings.TrimPrefix(line, "file://")
line, _ = url.QueryUnescape(line)
}
mf, err := mediaFileRepository.FindByPath(path)
if baseDir != "" && !filepath.IsAbs(line) {
line = filepath.Join(baseDir, line)
}
mf, err := mediaFileRepository.FindByPath(line)
if err != nil {
log.Warn(ctx, "Path in playlist not found", "playlist", pls.Name, "path", path, err)
log.Warn(ctx, "Path in playlist not found", "playlist", pls.Name, "path", line, err)
continue
}
mfs = append(mfs, *mf)
}
if pls.Name == "" {
pls.Name = time.Now().Format(time.RFC3339)
}
pls.Tracks = nil
pls.AddMediaFiles(mfs)

View File

@@ -2,6 +2,10 @@ package core
import (
"context"
"os"
"time"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/tests"
@@ -12,13 +16,16 @@ import (
var _ = Describe("Playlists", func() {
var ds model.DataStore
var ps Playlists
var mp mockedPlaylist
ctx := context.Background()
BeforeEach(func() {
mp = mockedPlaylist{}
ds = &tests.MockDataStore{
MockedMediaFile: &mockedMediaFile{},
MockedPlaylist: &mockedPlaylist{},
MockedPlaylist: &mp,
}
ctx = request.WithUser(ctx, model.User{ID: "123"})
})
Describe("ImportFile", func() {
@@ -29,10 +36,12 @@ var _ = Describe("Playlists", func() {
It("parses well-formed playlists", func() {
pls, err := ps.ImportFile(ctx, "tests/fixtures", "playlists/pls1.m3u")
Expect(err).To(BeNil())
Expect(pls.OwnerID).To(Equal("123"))
Expect(pls.Tracks).To(HaveLen(3))
Expect(pls.Tracks[0].Path).To(Equal("tests/fixtures/test.mp3"))
Expect(pls.Tracks[1].Path).To(Equal("tests/fixtures/test.ogg"))
Expect(pls.Tracks[2].Path).To(Equal("/tests/fixtures/01 Invisible (RED) Edit Version.mp3"))
Expect(mp.last).To(Equal(pls))
})
It("parses playlists using LF ending", func() {
@@ -48,6 +57,37 @@ var _ = Describe("Playlists", func() {
})
})
Describe("ImportM3U", func() {
BeforeEach(func() {
ps = NewPlaylists(ds)
ctx = request.WithUser(ctx, model.User{ID: "123"})
})
It("parses well-formed playlists", func() {
f, _ := os.Open("tests/fixtures/playlists/pls-post-with-name.m3u")
defer f.Close()
pls, err := ps.ImportM3U(ctx, f)
Expect(pls.OwnerID).To(Equal("123"))
Expect(pls.Name).To(Equal("playlist 1"))
Expect(err).To(BeNil())
Expect(pls.Tracks[0].Path).To(Equal("tests/fixtures/test.mp3"))
Expect(pls.Tracks[1].Path).To(Equal("tests/fixtures/test.ogg"))
Expect(pls.Tracks[2].Path).To(Equal("/tests/fixtures/01 Invisible (RED) Edit Version.mp3"))
Expect(mp.last).To(Equal(pls))
f.Close()
})
It("sets the playlist name as a timestamp if the #PLAYLIST directive is not present", func() {
f, _ := os.Open("tests/fixtures/playlists/pls-post.m3u")
defer f.Close()
pls, err := ps.ImportM3U(ctx, f)
Expect(err).To(BeNil())
_, err = time.Parse(time.RFC3339, pls.Name)
Expect(err).To(BeNil())
})
})
})
type mockedMediaFile struct {
@@ -62,6 +102,7 @@ func (r *mockedMediaFile) FindByPath(s string) (*model.MediaFile, error) {
}
type mockedPlaylist struct {
last *model.Playlist
model.PlaylistRepository
}
@@ -69,6 +110,7 @@ func (r *mockedPlaylist) FindByPath(string) (*model.Playlist, error) {
return nil, model.ErrNotFound
}
func (r *mockedPlaylist) Put(*model.Playlist) error {
func (r *mockedPlaylist) Put(pls *model.Playlist) error {
r.last = pls
return nil
}

View File

@@ -40,16 +40,16 @@ var _ = Describe("PlayTracker", func() {
tracker = newPlayTracker(ds, events.GetBroker())
track = model.MediaFile{
ID: "123",
Title: "Track Title",
Album: "Track Album",
AlbumID: "al-1",
Artist: "Track Artist",
ArtistID: "ar-1",
AlbumArtist: "Track AlbumArtist",
TrackNumber: 1,
Duration: 180,
MbzTrackID: "mbz-123",
ID: "123",
Title: "Track Title",
Album: "Track Album",
AlbumID: "al-1",
Artist: "Track Artist",
ArtistID: "ar-1",
AlbumArtist: "Track AlbumArtist",
TrackNumber: 1,
Duration: 180,
MbzRecordingID: "mbz-123",
}
_ = ds.MediaFile(ctx).Put(&track)
artist = model.Artist{ID: "ar-1"}

View File

@@ -0,0 +1,27 @@
package migrations
import (
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upRenameMusicbrainzRecordingId, downRenameMusicbrainzRecordingId)
}
func upRenameMusicbrainzRecordingId(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table media_file
rename column mbz_track_id to mbz_recording_id;
`)
return err
}
func downRenameMusicbrainzRecordingId(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table media_file
rename column mbz_recording_id to mbz_track_id;
`)
return err
}

77
go.mod
View File

@@ -1,12 +1,13 @@
module github.com/navidrome/navidrome
go 1.19
go 1.20
require (
code.cloudfoundry.org/go-diodes v0.0.0-20190809170250-f77fb823c7ee
github.com/DexterLB/mpvipc v0.0.0-20221227161445-38b9935eae9d
github.com/Masterminds/squirrel v1.5.4
github.com/ReneKroon/ttlcache/v2 v2.11.0
github.com/beego/beego/v2 v2.0.7
github.com/beego/beego/v2 v2.1.3
github.com/bradleyjkemp/cupaloy/v2 v2.8.0
github.com/deluan/rest v0.0.0-20211101235434-380523c4bb47
github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1
@@ -15,38 +16,40 @@ require (
github.com/djherbis/atime v1.1.0
github.com/djherbis/fscache v0.10.2-0.20220222230828-2909c950912d
github.com/djherbis/stream v1.4.0
github.com/djherbis/times v1.6.0
github.com/dustin/go-humanize v1.0.1
github.com/faiface/beep v1.1.0
github.com/fatih/structs v1.1.0
github.com/go-chi/chi/v5 v5.0.8
github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/cors v1.2.1
github.com/go-chi/httprate v0.7.4
github.com/go-chi/jwtauth/v5 v5.1.0
github.com/google/uuid v1.3.0
github.com/go-chi/jwtauth/v5 v5.1.1
github.com/google/uuid v1.4.0
github.com/google/wire v0.5.0
github.com/hashicorp/go-multierror v1.1.1
github.com/kr/pretty v0.3.1
github.com/lestrrat-go/jwx/v2 v2.0.9
github.com/lestrrat-go/jwx/v2 v2.0.16
github.com/matoous/go-nanoid/v2 v2.0.0
github.com/mattn/go-sqlite3 v1.14.16
github.com/mattn/go-sqlite3 v1.14.18
github.com/mattn/go-zglob v0.0.3
github.com/microcosm-cc/bluemonday v1.0.24
github.com/mileusna/useragent v1.3.2
github.com/onsi/ginkgo/v2 v2.9.5
github.com/onsi/gomega v1.27.7
github.com/microcosm-cc/bluemonday v1.0.23
github.com/mileusna/useragent v1.3.4
github.com/onsi/ginkgo/v2 v2.13.1
github.com/onsi/gomega v1.30.0
github.com/pressly/goose/v3 v3.11.2
github.com/prometheus/client_golang v1.15.1
github.com/prometheus/client_golang v1.17.0
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.2
github.com/spf13/cobra v1.7.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.3
github.com/stretchr/testify v1.8.4
github.com/unrolled/secure v1.13.0
github.com/xrash/smetrics v0.0.0-20200730060457-89a2a8a1fb0b
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
golang.org/x/image v0.7.0
golang.org/x/sync v0.2.0
golang.org/x/text v0.9.0
golang.org/x/tools v0.9.1
golang.org/x/image v0.14.0
golang.org/x/sync v0.5.0
golang.org/x/text v0.14.0
golang.org/x/tools v0.15.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -55,38 +58,44 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/hajimehoshi/go-mp3 v0.3.4 // indirect
github.com/hajimehoshi/oto v1.0.1 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/icza/bitio v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mewkiz/flac v1.0.7 // indirect
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
@@ -94,11 +103,15 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
go.uber.org/goleak v1.1.11 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
golang.org/x/crypto v0.15.0 // indirect
golang.org/x/exp/shiny v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
golang.org/x/mobile v0.0.0-20230427221453-e8d11dd0ba41 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/sys v0.14.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
)

186
go.sum
View File

@@ -40,14 +40,17 @@ code.cloudfoundry.org/go-diodes v0.0.0-20190809170250-f77fb823c7ee/go.mod h1:Jzi
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
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/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/DexterLB/mpvipc v0.0.0-20221227161445-38b9935eae9d h1:UyefntSsjbYaTDUdZF4A1vPZX3Xpnewv6JNBzQPYAzY=
github.com/DexterLB/mpvipc v0.0.0-20221227161445-38b9935eae9d/go.mod h1:nMVB54ifXmC1hpgfq7gTpotbv891pd2wAX/whuUj1q4=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/ReneKroon/ttlcache/v2 v2.11.0 h1:OvlcYFYi941SBN3v9dsDcC2N8vRxyHcCmJb3Vl4QMoM=
github.com/ReneKroon/ttlcache/v2 v2.11.0/go.mod h1:mBxvsNY+BT8qLLd6CuAJubbKo6r0jh3nb5et22bbfGY=
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/beego/v2 v2.0.7 h1:9KNnUM40tn3pbCOFfe6SJ1oOL0oTi/oBS/C/wCEdAXA=
github.com/beego/beego/v2 v2.0.7/go.mod h1:f0uOEkmJWgAuDTlTxUdgJzwG3PDSIf3UWF3NpMohbFE=
github.com/beego/beego/v2 v2.1.3 h1:x436yz6jrSasYBzfOP39S097kvq5/5fBTFfEvVA456M=
github.com/beego/beego/v2 v2.1.3/go.mod h1:0J0RQVIpepnRUfu6ax+kLVVB1FcdYryHK9lpRl5wvbY=
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/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
@@ -62,14 +65,15 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
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/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/deluan/rest v0.0.0-20211101235434-380523c4bb47 h1:IhGAYGDi212gspq0XkYAI+DN5e9lfAIm8Qgu1wj9yN4=
github.com/deluan/rest v0.0.0-20211101235434-380523c4bb47/go.mod h1:tSgDythFsl0QgS/PFWfIZqcJKnkADWneY80jaVRlqK8=
github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1 h1:mGvOb3zxl4vCLv+dbf7JA6CAaM2UH/AGP1KX4DsJmTI=
@@ -84,6 +88,8 @@ github.com/djherbis/fscache v0.10.2-0.20220222230828-2909c950912d h1:eAikRiT337j
github.com/djherbis/fscache v0.10.2-0.20220222230828-2909c950912d/go.mod h1:+uJNKpxCg52qVRGr+srICjiY8QvV0riatTzCGMUuSEY=
github.com/djherbis/stream v1.4.0 h1:aVD46WZUiq5kJk55yxJAyw6Kuera6kmC3i2vEQyW/AE=
github.com/djherbis/stream v1.4.0/go.mod h1:cqjC1ZRq3FFwkGmUtHwcldbnW8f0Q4YuVsGW1eAFtOk=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -92,6 +98,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
@@ -99,19 +107,24 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/httprate v0.7.4 h1:a2GIjv8he9LRf3712zxxnRdckQCm7I8y8yQhkJ84V6M=
github.com/go-chi/httprate v0.7.4/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A=
github.com/go-chi/jwtauth/v5 v5.1.0 h1:wJyf2YZ/ohPvNJBwPOzZaQbyzwgMZZceE1m8FOzXLeA=
github.com/go-chi/jwtauth/v5 v5.1.0/go.mod h1:MA93hc1au3tAQwCKry+fI4LqJ5MIVN4XSsglOo+lSc8=
github.com/go-chi/jwtauth/v5 v5.1.1 h1:Pjixqu5YkjE9sCLpzE01L0Q4sQzJIPdo7uz9r8ftp/c=
github.com/go-chi/jwtauth/v5 v5.1.1/go.mod h1:CYP1WSbzD4MPuKCr537EM3kfFhSQgpUEtMJFuYJjqWU=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
@@ -158,8 +171,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -178,8 +191,8 @@ github.com/google/pprof v0.0.0-20230323073829-e72429f035bd/go.mod h1:79YE0hCXdHa
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.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8=
github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -189,6 +202,14 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa
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/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68=
github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo=
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
github.com/hajimehoshi/oto v1.0.1 h1:8AMnq0Yr2YmzaiqTg/k1Yzd6IygUGk2we9nmjgbgPn4=
github.com/hajimehoshi/oto v1.0.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
@@ -202,8 +223,14 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/icza/bitio v1.0.0 h1:squ/m1SHyFeCA6+6Gyol1AxV9nmPPlJFT8c2vKdj3U8=
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k=
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
@@ -223,36 +250,42 @@ 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/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.0.9 h1:TRX4Q630UXxPVLvP5vGaqVJO7S+0PE6msRZUsFSBoC8=
github.com/lestrrat-go/jwx/v2 v2.0.9/go.mod h1:K68euYaR95FnL0hIQB8VvzL70vB7pSifbJUydCTPmgM=
github.com/lestrrat-go/jwx/v2 v2.0.16 h1:TuH3dBkYTy2giQg/9D8f20znS3JtMRuQJ372boS3lWk=
github.com/lestrrat-go/jwx/v2 v2.0.16/go.mod h1:jBHyESp4e7QxfERM0UKkQ80/94paqNIEcdEfiUYz5zE=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0=
github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-zglob v0.0.3 h1:6Ry4EYsScDyt5di4OI6xw1bYhOqfE5S33Z1OPy+d+To=
github.com/mattn/go-zglob v0.0.3/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/microcosm-cc/bluemonday v1.0.24 h1:NGQoPtwGVcbGkKfvyYk1yRqknzBuoMiUrO6R7uFTPlw=
github.com/microcosm-cc/bluemonday v1.0.24/go.mod h1:ArQySAMps0790cHSkdPEJ7bGkF2VePWH773hsJNSHf8=
github.com/mileusna/useragent v1.3.2 h1:yGBQVNkyrlnSe4l0rlaQoH8XlG9xDkc6a7ygwPxALoU=
github.com/mileusna/useragent v1.3.2/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
github.com/mewkiz/flac v1.0.7 h1:uIXEjnuXqdRaZttmSFM5v5Ukp4U6orrZsnYGGR3yow8=
github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 h1:EyTNMdePWaoWsRSGQnXiSoQu0r6RS1eA557AwJhlzHU=
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY=
github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4=
github.com/mileusna/useragent v1.3.4 h1:MiuRRuvGjEie1+yZHO88UBYg8YBC/ddF6T7F56i3PCk=
github.com/mileusna/useragent v1.3.4/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -262,15 +295,16 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/ginkgo/v2 v2.13.1 h1:LNGfMbR2OVGBfXjvRZIZ2YCTQdGKtPLvuI1rMCCj3OU=
github.com/onsi/ginkgo/v2 v2.13.1/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4=
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
@@ -278,27 +312,30 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pressly/goose/v3 v3.11.2 h1:QgTP45FhBBHdmf7hWKlbWFHtwPtxo0phSDkwDKGUrYs=
github.com/pressly/goose/v3 v3.11.2/go.mod h1:LWQzSc4vwfHA/3B8getTp8g3J5Z8tFBxgxinmGlMlJk=
github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 h1:DAYUYH5869yV94zvCES9F51oYtN5oGlwjxJJz7ZCnik=
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
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/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
@@ -307,8 +344,8 @@ github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
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.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -328,13 +365,14 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/unrolled/secure v1.13.0 h1:sdr3Phw2+f8Px8HE5sd1EHdj1aV3yUwed/uZXChLFsk=
github.com/unrolled/secure v1.13.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/xrash/smetrics v0.0.0-20200730060457-89a2a8a1fb0b h1:tnWgqoOBmInkt5pbLjagwNVjjT4RdJhFHzL1ebCSRh8=
github.com/xrash/smetrics v0.0.0-20200730060457-89a2a8a1fb0b/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -360,9 +398,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
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=
@@ -375,11 +413,14 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
golang.org/x/exp/shiny v0.0.0-20230515195305-f3d0a9c9a5cc h1:JMi0oO0NoPZTAzHSdkdUoHbdcLfo9nPtK37kzE6I3Hk=
golang.org/x/exp/shiny v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
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/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
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=
@@ -392,7 +433,10 @@ golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPI
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20230427221453-e8d11dd0ba41 h1:539vykMVJsmdiucRtMmdeLLZaTVhWhaAHFcPabj2lws=
golang.org/x/mobile v0.0.0-20230427221453-e8d11dd0ba41/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
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=
@@ -404,8 +448,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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=
@@ -442,9 +486,9 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
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=
@@ -467,18 +511,20 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/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-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -511,18 +557,23 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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=
@@ -531,9 +582,10 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -592,8 +644,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -688,8 +740,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -697,6 +749,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
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.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -49,7 +49,7 @@ type ArtistIndexes []ArtistIndex
type ArtistRepository interface {
CountAll(options ...QueryOptions) (int64, error)
Exists(id string) (bool, error)
Put(m *Artist) error
Put(m *Artist, colsToUpdate ...string) error
Get(id string) (*Artist, error)
GetAll(options ...QueryOptions) (Artists, error)
Search(q string, offset int, size int) (Artists, error)

View File

@@ -23,6 +23,7 @@ var _ = Describe("Criteria", func() {
All{
StartsWith{"comment": "this"},
InTheRange{"year": []int{1980, 1990}},
IsNot{"genre": "test"},
},
},
Sort: "title",
@@ -43,7 +44,8 @@ var _ = Describe("Criteria", func() {
},
{ "all": [
{ "startsWith": {"comment": "this"} },
{ "inTheRange": {"year":[1980,1990]} }
{ "inTheRange": {"year":[1980,1990]} },
{ "isNot": { "genre": "test" }}
]
}
],
@@ -62,8 +64,8 @@ var _ = Describe("Criteria", func() {
It("generates valid SQL", func() {
sql, args, err := goObj.ToSql()
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Expect(sql).To(gomega.Equal("(media_file.title LIKE ? AND media_file.title NOT LIKE ? AND (media_file.artist <> ? OR media_file.album = ?) AND (media_file.comment LIKE ? AND (media_file.year >= ? AND media_file.year <= ?)))"))
gomega.Expect(args).To(gomega.ConsistOf("%love%", "%hate%", "u2", "best of", "this%", 1980, 1990))
gomega.Expect(sql).To(gomega.Equal("(media_file.title LIKE ? AND media_file.title NOT LIKE ? AND (media_file.artist <> ? OR media_file.album = ?) AND (media_file.comment LIKE ? AND (media_file.year >= ? AND media_file.year <= ?) AND COALESCE(genre.name, '') <> ?))"))
gomega.Expect(args).To(gomega.ConsistOf("%love%", "%hate%", "u2", "best of", "this%", 1980, 1990, "test"))
})
It("marshals to JSON", func() {

View File

@@ -40,7 +40,7 @@ var fieldMap = map[string]*mappedField{
"bitrate": {field: "media_file.bit_rate"},
"bpm": {field: "media_file.bpm"},
"channels": {field: "media_file.channels"},
"genre": {field: "genre.name"},
"genre": {field: "COALESCE(genre.name, '')"},
"loved": {field: "COALESCE(annotation.starred, false)"},
"dateloved": {field: "annotation.starred_at"},
"lastplayed": {field: "annotation.play_date"},

View File

@@ -59,7 +59,7 @@ type MediaFile struct {
Lyrics string `structs:"lyrics" json:"lyrics,omitempty"`
Bpm int `structs:"bpm" json:"bpm,omitempty"`
CatalogNum string `structs:"catalog_num" json:"catalogNum,omitempty"`
MbzTrackID string `structs:"mbz_track_id" json:"mbzTrackId,omitempty" orm:"column(mbz_track_id)"`
MbzRecordingID string `structs:"mbz_recording_id" json:"mbzRecordingID,omitempty" orm:"column(mbz_recording_id)"`
MbzReleaseTrackID string `structs:"mbz_release_track_id" json:"mbzReleaseTrackId,omitempty" orm:"column(mbz_release_track_id)"`
MbzAlbumID string `structs:"mbz_album_id" json:"mbzAlbumId,omitempty" orm:"column(mbz_album_id)"`
MbzArtistID string `structs:"mbz_artist_id" json:"mbzArtistId,omitempty" orm:"column(mbz_artist_id)"`

View File

@@ -60,10 +60,10 @@ func (r *artistRepository) Exists(id string) (bool, error) {
return r.exists(Select().Where(Eq{"artist.id": id}))
}
func (r *artistRepository) Put(a *model.Artist) error {
func (r *artistRepository) Put(a *model.Artist, colsToUpdate ...string) error {
a.FullText = getFullText(a.Name, a.SortArtistName)
dba := r.fromModel(a)
_, err := r.put(dba.ID, dba)
_, err := r.put(dba.ID, dba, colsToUpdate...)
if err != nil {
return err
}

View File

@@ -54,7 +54,11 @@
"comment": "التعليق",
"rating": "التقييم",
"createdAt": "تاريخ الإضافة",
"size": "الحجم"
"size": "الحجم",
"originalDate": "",
"releaseDate": "",
"releases": "",
"released": ""
},
"actions": {
"playAll": "شغّل",

View File

@@ -54,7 +54,11 @@
"comment": "Коментар",
"rating": "Рейтинг",
"createdAt": "Добавено на",
"size": "Размер"
"size": "Размер",
"originalDate": "",
"releaseDate": "",
"releases": "",
"released": ""
},
"actions": {
"playAll": "Пусни",

View File

@@ -54,7 +54,11 @@
"comment": "Comentari",
"rating": "Valoració",
"createdAt": "",
"size": ""
"size": "",
"originalDate": "",
"releaseDate": "",
"releases": "",
"released": ""
},
"actions": {
"playAll": "Reprodueix",

View File

@@ -26,7 +26,7 @@
"bpm": "BPM",
"playDate": "Poslední přehravaná skladba",
"channels": "Kanály",
"createdAt": ""
"createdAt": "Přidáno"
},
"actions": {
"addToQueue": "Přehrát později",
@@ -53,8 +53,12 @@
"updatedAt": "Aktualizováno",
"comment": "Komentář",
"rating": "Hodnocení",
"createdAt": "",
"size": ""
"createdAt": "Přidáno",
"size": "Velikost",
"originalDate": "Původní",
"releaseDate": "Vydáno",
"releases": "Vydání |||| Vydání",
"released": "Vydáno"
},
"actions": {
"playAll": "Přehrát",
@@ -64,7 +68,7 @@
"addToPlaylist": "Přidat do seznamu skladeb",
"download": "Stáhnout",
"info": "Získat informace",
"share": ""
"share": "Sdílet"
},
"lists": {
"all": "Všechno",
@@ -85,7 +89,7 @@
"playCount": "Přehrání",
"rating": "Hodnocení",
"genre": "Žánr",
"size": ""
"size": "Velikost"
}
},
"user": {
@@ -165,33 +169,33 @@
}
},
"radio": {
"name": "",
"name": "Rádio |||| Rádia",
"fields": {
"name": "",
"streamUrl": "",
"homePageUrl": "",
"updatedAt": "",
"createdAt": ""
"name": "Název",
"streamUrl": "URL streamu",
"homePageUrl": "URL stránky",
"updatedAt": "Nahráno",
"createdAt": "Vytvořeno"
},
"actions": {
"playNow": ""
"playNow": "Spustit"
}
},
"share": {
"name": "",
"name": "Sdílení |||| Sdílení",
"fields": {
"username": "",
"url": "",
"description": "",
"contents": "",
"expiresAt": "",
"lastVisitedAt": "",
"visitCount": "",
"format": "",
"maxBitRate": "",
"updatedAt": "",
"createdAt": "",
"downloadable": ""
"username": "Sdíleno",
"url": "URL",
"description": "Popis",
"contents": "Obsah",
"expiresAt": "Vyprší",
"lastVisitedAt": "Naposledy navštíveno",
"visitCount": "Počet návšev",
"format": "Formát",
"maxBitRate": "Max. Bit Rate",
"updatedAt": "Nahráno",
"createdAt": "Vytvořeno",
"downloadable": "Povolit stahování?"
}
}
},
@@ -222,7 +226,7 @@
"oneOf": "Musí splňovat jedno z: %{options}",
"regex": "Musí být ve specifickém formátu (regexp): %{pattern}",
"unique": "Musí být jedinečný",
"url": ""
"url": "Musí být platná URL"
},
"action": {
"add_filter": "Přidat filtr",
@@ -252,9 +256,9 @@
"close_menu": "Zavřít nabídku",
"unselect": "Zrušit výběr",
"skip": "Přeskočit",
"bulk_actions_mobile": "",
"share": "",
"download": ""
"bulk_actions_mobile": "1 |||| %{smart_count}",
"share": "Sdílet",
"download": "Stáhnout"
},
"boolean": {
"true": "Ano",
@@ -362,14 +366,14 @@
"listenBrainzLinkFailure": "ListenBrainz nemohlo být připojeno: %{error}",
"listenBrainzUnlinkSuccess": "ListenBrainz odpojeno a scrobblování vypnuto",
"listenBrainzUnlinkFailure": "ListenBrainz nemohlo být odpojeno",
"downloadOriginalFormat": "",
"shareOriginalFormat": "",
"shareDialogTitle": "",
"shareBatchDialogTitle": "",
"shareSuccess": "",
"shareFailure": "",
"downloadDialogTitle": "",
"shareCopyToClipboard": ""
"downloadOriginalFormat": "Stáhnout v původním formátu",
"shareOriginalFormat": "Sdílet v původním formátu",
"shareDialogTitle": "Sdílet %{resource} '%{name}'",
"shareBatchDialogTitle": "Sdílet 1 %{resource} |||| Sdílet %{smart_count} %{resource}",
"shareSuccess": "URL zkopírována do schránky: %{url}",
"shareFailure": "Chyba při kopírování URL %{url} do schránky",
"downloadDialogTitle": "Stáhnout %{resource} '%{name}' (%{size})",
"shareCopyToClipboard": "Zkopírovat do schránky: Ctrl+C, Enter"
},
"menu": {
"library": "Knihovna",
@@ -385,12 +389,12 @@
"desktop_notifications": "Oznámení na ploše",
"lastfmScrobbling": "Scrobblovat na Last.fm",
"listenBrainzScrobbling": "Scrobblovat na ListenBrainz",
"replaygain": "",
"preAmp": "",
"replaygain": "Mód ReplayGain",
"preAmp": "ReplayGain PreAmp (dB)",
"gain": {
"none": "",
"album": "",
"track": ""
"none": "Vypnuto",
"album": "Použít Album Gain",
"track": "Použít Track Gain"
}
}
},

View File

@@ -54,7 +54,11 @@
"comment": "Kommentar",
"rating": "",
"createdAt": "",
"size": ""
"size": "",
"originalDate": "",
"releaseDate": "",
"releases": "",
"released": ""
},
"actions": {
"playAll": "Afspil",

View File

@@ -54,7 +54,11 @@
"comment": "Kommentar",
"rating": "Bewertung",
"createdAt": "Hinzugefügt",
"size": "Größe"
"size": "Größe",
"originalDate": "Ursprünglich",
"releaseDate": "Erschienen",
"releases": "Veröffentlichung |||| Veröffentlichungen",
"released": "Erschienen"
},
"actions": {
"playAll": "Abspielen",

View File

@@ -54,7 +54,11 @@
"comment": "Komento",
"rating": "",
"createdAt": "",
"size": ""
"size": "",
"originalDate": "",
"releaseDate": "",
"releases": "",
"released": ""
},
"actions": {
"playAll": "Ludi",

View File

@@ -54,7 +54,11 @@
"comment": "Comentario",
"rating": "Calificación",
"createdAt": "Creado el",
"size": "Tamaño del archivo"
"size": "Tamaño del archivo",
"originalDate": "Original",
"releaseDate": "Publicado",
"releases": "Lanzamiento |||| Lanzamientos",
"released": "Publicado"
},
"actions": {
"playAll": "Reproducir",
@@ -181,7 +185,7 @@
"name": "Compartir",
"fields": {
"username": "Nombre de usuario",
"url": "",
"url": "URL",
"description": "Descripción",
"contents": "Contenido",
"expiresAt": "Caduca el",
@@ -191,7 +195,7 @@
"maxBitRate": "Tasa de bits Máx.",
"updatedAt": "Actualizado el",
"createdAt": "Creado el",
"downloadable": ""
"downloadable": "¿Permitir descargas?"
}
}
},
@@ -222,7 +226,7 @@
"oneOf": "Debe ser uno de: %{options}",
"regex": "Debe coincidir con un formato específico (regexp): %{pattern}",
"unique": "Tiene que ser único",
"url": ""
"url": "Debe ser una URL válida"
},
"action": {
"add_filter": "Añadir filtro",
@@ -252,7 +256,7 @@
"close_menu": "Cerrar menú",
"unselect": "Deseleccionado",
"skip": "Omitir",
"bulk_actions_mobile": "",
"bulk_actions_mobile": "1 |||| %{smart_count}",
"share": "Compartir",
"download": "Descargar"
},
@@ -364,12 +368,12 @@
"listenBrainzUnlinkFailure": "No se pudo desconectar ListenBrainz",
"downloadOriginalFormat": "Descargar formato original",
"shareOriginalFormat": "Compartir formato original",
"shareDialogTitle": "",
"shareBatchDialogTitle": "",
"shareSuccess": "",
"shareFailure": "",
"downloadDialogTitle": "",
"shareCopyToClipboard": ""
"shareDialogTitle": "Compartir %{resource} '%{name}'",
"shareBatchDialogTitle": "Compartir 1 %{resource} |||| Share %{smart_count} %{resource}",
"shareSuccess": "URL copiada al portapapeles: %{url}",
"shareFailure": "Error al copiar la URL %{url} al portapapeles",
"downloadDialogTitle": "Descargar %{resource} '%{name}' (%{size})",
"shareCopyToClipboard": "Copiar al portapapeles: Ctrl+C, Intro"
},
"menu": {
"library": "Biblioteca",
@@ -385,8 +389,8 @@
"desktop_notifications": "Notificaciones de escritorio",
"lastfmScrobbling": "Scrobble a Last.fm",
"listenBrainzScrobbling": "Scrobble a ListenBrainz",
"replaygain": "",
"preAmp": "",
"replaygain": "Modo de ReplayGain",
"preAmp": "ReplayGain PreAmp (dB)",
"gain": {
"none": "Ninguno",
"album": "Álbum",

View File

@@ -54,7 +54,11 @@
"comment": "اظهارنظر",
"rating": "رتبه بندی",
"createdAt": "",
"size": ""
"size": "",
"originalDate": "",
"releaseDate": "",
"releases": "",
"released": ""
},
"actions": {
"playAll": "پخش",

View File

@@ -54,7 +54,11 @@
"comment": "Kommentti",
"rating": "Arvostelu",
"createdAt": "Lisätty",
"size": "Koko"
"size": "Koko",
"originalDate": "Alkuperäinen",
"releaseDate": "Julkaistu",
"releases": "Julkaisu |||| Julkaisut",
"released": "Julkaistu"
},
"actions": {
"playAll": "Soita",

View File

@@ -26,7 +26,7 @@
"bpm": "BPM",
"playDate": "Derniers joués",
"channels": "Canaux",
"createdAt": ""
"createdAt": "Date d'ajout"
},
"actions": {
"addToQueue": "Ajouter à la file",
@@ -53,8 +53,12 @@
"updatedAt": "Mis à jour le",
"comment": "Commentaire",
"rating": "Classement",
"createdAt": "",
"size": ""
"createdAt": "Date d'ajout",
"size": "Taille",
"originalDate": "Original",
"releaseDate": "Sortie",
"releases": "Sortie |||| Sorties",
"released": "Sortie"
},
"actions": {
"playAll": "Lire",
@@ -64,7 +68,7 @@
"addToPlaylist": "Ajouter à la playlist",
"download": "Télécharger",
"info": "Plus d'informations",
"share": ""
"share": "Partager"
},
"lists": {
"all": "Tous",
@@ -85,7 +89,7 @@
"playCount": "Lectures",
"rating": "Classement",
"genre": "Genre",
"size": ""
"size": "Taille"
}
},
"user": {
@@ -165,33 +169,33 @@
}
},
"radio": {
"name": "",
"name": "Radio |||| Radios",
"fields": {
"name": "",
"streamUrl": "",
"homePageUrl": "",
"updatedAt": "",
"createdAt": ""
"name": "Nom",
"streamUrl": "Lien du stream",
"homePageUrl": "Lien de la page d'accueil",
"updatedAt": "Mis à jour le",
"createdAt": "Créée le"
},
"actions": {
"playNow": ""
"playNow": "Jouer"
}
},
"share": {
"name": "",
"name": "Partage |||| Partages",
"fields": {
"username": "",
"url": "",
"description": "",
"contents": "",
"expiresAt": "",
"lastVisitedAt": "",
"visitCount": "",
"format": "",
"username": "Partagé(e) par",
"url": "Lien URL",
"description": "Description",
"contents": "Contenu",
"expiresAt": "Expire le",
"lastVisitedAt": "Visité pour la dernière fois",
"visitCount": "Nombre de visites",
"format": "Format",
"maxBitRate": "",
"updatedAt": "",
"createdAt": "",
"downloadable": ""
"updatedAt": "Mis à jour le",
"createdAt": "Créé le",
"downloadable": "Autoriser les téléchargements?"
}
}
},
@@ -222,7 +226,7 @@
"oneOf": "Doit être au choix : %{options}",
"regex": "Doit respecter un format spécifique (regexp) : %{pattern}",
"unique": "Doit être unique",
"url": ""
"url": "Doit être un lien URL correct"
},
"action": {
"add_filter": "Ajouter un filtre",
@@ -252,9 +256,9 @@
"close_menu": "Fermer le menu",
"unselect": "Désélectionner",
"skip": "Ignorer",
"bulk_actions_mobile": "",
"share": "",
"download": ""
"bulk_actions_mobile": "1 |||| %{smart_count}",
"share": "Partager",
"download": "Télécharger"
},
"boolean": {
"true": "Oui",
@@ -362,14 +366,14 @@
"listenBrainzLinkFailure": "Échec lors de la liaison avec ListenBrainz: %{error}",
"listenBrainzUnlinkSuccess": "La liaison et le scrobble avec ListenBrainz sont maintenant désactivés",
"listenBrainzUnlinkFailure": "Échec lors de la désactivation de la liaison avec ListenBrainz",
"downloadOriginalFormat": "",
"shareOriginalFormat": "",
"shareDialogTitle": "",
"shareBatchDialogTitle": "",
"shareSuccess": "",
"shareFailure": "",
"downloadDialogTitle": "",
"shareCopyToClipboard": ""
"downloadOriginalFormat": "Télécharger au format original",
"shareOriginalFormat": "Partager avec le format original",
"shareDialogTitle": "Partager %{resource} '%{name}'",
"shareBatchDialogTitle": "Partager 1 %{resource} |||| Partager %{smart_count} %{resource}",
"shareSuccess": "Lien copié vers le presse-papier: %{url}",
"shareFailure": "Erreur en copiant le lien %{url} vers le presse-papier",
"downloadDialogTitle": "Télécharger %{resource} '%{name}' (%{size})",
"shareCopyToClipboard": "Copier vers le presse-papier: Ctrl+C, Enter"
},
"menu": {
"library": "Bibliothèque",
@@ -388,7 +392,7 @@
"replaygain": "",
"preAmp": "",
"gain": {
"none": "",
"none": "Désactivé",
"album": "",
"track": ""
}
@@ -450,7 +454,7 @@
"vol_up": "Augmenter le volume",
"vol_down": "Baisser le volume",
"toggle_love": "Ajouter/Enlever le morceau des favoris",
"current_song": ""
"current_song": "Aller à la chanson en cours"
}
}
}

View File

@@ -54,7 +54,11 @@
"comment": "Comentario",
"rating": "Valoración",
"createdAt": "Engadido o",
"size": "Tamaño"
"size": "Tamaño",
"originalDate": "Orixinal",
"releaseDate": "Publicado",
"releases": "Publicación ||| Publicacións",
"released": "Publicado"
},
"actions": {
"playAll": "Reproducir",
@@ -178,7 +182,7 @@
}
},
"share": {
"name": "´",
"name": "Compartición ||| Comparticións",
"fields": {
"username": "Compartida por",
"url": "URL",

View File

@@ -54,7 +54,11 @@
"comment": "Commento",
"rating": "Valutazione",
"createdAt": "",
"size": ""
"size": "",
"originalDate": "",
"releaseDate": "",
"releases": "",
"released": ""
},
"actions": {
"playAll": "Riproduci",

View File

@@ -54,7 +54,11 @@
"comment": "コメント",
"rating": "レート",
"createdAt": "追加日",
"size": "サイズ"
"size": "サイズ",
"originalDate": "オリジナルの日付",
"releaseDate": "リリース日",
"releases": "リリース",
"released": "リリース"
},
"actions": {
"playAll": "再生",
@@ -191,7 +195,7 @@
"maxBitRate": "最大ビットレート",
"updatedAt": "更新日",
"createdAt": "作成日",
"downloadable": ""
"downloadable": "ダウンロードを許可しますか?"
}
}
},
@@ -369,7 +373,7 @@
"shareSuccess": "コピーしました: %{url}",
"shareFailure": "コピーに失敗しました %{url}",
"downloadDialogTitle": "ダウンロード %{resource} '%{name}' (%{size})",
"shareCopyToClipboard": ""
"shareCopyToClipboard": "クリップボードへコピー: Ctrl+C, Enter"
},
"menu": {
"library": "ライブラリ",

460
resources/i18n/ko.json Normal file
View File

@@ -0,0 +1,460 @@
{
"languageName": "한국어",
"resources": {
"song": {
"name": "곡",
"fields": {
"albumArtist": "앨범 아티스트",
"duration": "길이",
"trackNumber": "#",
"playCount": "재생 수",
"title": "제목",
"artist": "아티스트",
"album": "앨범",
"path": "파일 경로",
"genre": "장르",
"compilation": "Compilation",
"year": "년",
"size": "파일 크기",
"updatedAt": "업데이트 날짜",
"bitRate": "비트레이트",
"discSubtitle": "디스크 서브타이틀",
"starred": "좋아요",
"comment": "코멘트",
"rating": "평가",
"quality": "품질",
"bpm": "BPM",
"playDate": "마지막 재생",
"channels": "채널",
"createdAt": "추가 날짜"
},
"actions": {
"addToQueue": "마지막에 재생",
"playNow": "바로 재생",
"addToPlaylist": "플레이리스트에 추가",
"shuffleAll": "모든 곡 셔플",
"download": "다운로드",
"playNext": "다음에 재생",
"info": "상세 정보"
}
},
"album": {
"name": "앨범",
"fields": {
"albumArtist": "앨범 아티스트",
"artist": "아티스트",
"duration": "길이",
"songCount": "곡",
"playCount": "재생 수",
"name": "이름",
"genre": "장르",
"compilation": "Compilation",
"year": "년",
"updatedAt": "업데이트 날짜",
"comment": "코멘트",
"rating": "평가",
"createdAt": "추가 날짜",
"size": "크기",
"originalDate": "",
"releaseDate": "",
"releases": "",
"released": ""
},
"actions": {
"playAll": "재생",
"playNext": "다음에 재생",
"addToQueue": "마지막에 재생",
"shuffle": "셔플",
"addToPlaylist": "플레이리스트에 추가",
"download": "다운로드",
"info": "상세 정보",
"share": "공유"
},
"lists": {
"all": "전체",
"random": "랜덤",
"recentlyAdded": "최근 추가",
"recentlyPlayed": "최근 재생",
"mostPlayed": "가장 많이 재생",
"starred": "좋아요",
"topRated": "높은 평가"
}
},
"artist": {
"name": "아티스트",
"fields": {
"name": "이름",
"albumCount": "앨범 수",
"songCount": "곡 수",
"playCount": "재생 수",
"rating": "평가",
"genre": "장르",
"size": "크기"
}
},
"user": {
"name": "사용자",
"fields": {
"userName": "사용자명",
"isAdmin": "관리자",
"lastLoginAt": "최종 로그인",
"updatedAt": "업데이트 날짜",
"name": "이름",
"password": "비밀번호",
"createdAt": "생성 날짜",
"changePassword": "비밀번호를 변경하시겠습니까?",
"currentPassword": "현재 비밀번호",
"newPassword": "새로운 비밀번호",
"token": "토큰"
},
"helperTexts": {
"name": "이름 변경은 다음 로그인 이후에 반영됩니다"
},
"notifications": {
"created": "사용자가 생성되었습니다",
"updated": "사용자가 업데이트되었습니다",
"deleted": "사용자가 삭제되었습니다"
},
"message": {
"listenBrainzToken": "ListenBrainz 사용자 토큰을 입력하세요",
"clickHereForToken": "여기를 클릭하여 토큰을 얻으세요"
}
},
"player": {
"name": "플레이어",
"fields": {
"name": "이름",
"transcodingId": "트랜스코딩",
"maxBitRate": "최대 비트레이트",
"client": "클라이언트",
"userName": "사용자명",
"lastSeen": "마지막 사용",
"reportRealPath": "실제 파일 경로 반환",
"scrobbleEnabled": "다른 서비스에 scrobble"
}
},
"transcoding": {
"name": "트랜스코딩",
"fields": {
"name": "이름",
"targetFormat": "대상 포맷",
"defaultBitRate": "기본 비트레이트",
"command": "명령"
}
},
"playlist": {
"name": "플레이리스트",
"fields": {
"name": "이름",
"duration": "시간",
"ownerName": "소유자",
"public": "공개",
"updatedAt": "업데이트 날짜",
"createdAt": "생성 날짜",
"songCount": "곡",
"comment": "코멘트",
"sync": "자동 임포트",
"path": "임포트 원본"
},
"actions": {
"selectPlaylist": "플레이리스트 선택",
"addNewPlaylist": "'%{name}' 생성",
"export": "내보내기",
"makePublic": "공개하기",
"makePrivate": "비공개로 전환하기"
},
"message": {
"duplicate_song": "중복된 곡 추가",
"song_exist": "이미 플레이리스트에 존재하는 곡입니다. 추가하시겠습니까?"
}
},
"radio": {
"name": "라디오",
"fields": {
"name": "이름",
"streamUrl": "스트리밍 URL",
"homePageUrl": "홈페이지 URL",
"updatedAt": "업데이트 날짜",
"createdAt": "생성 날짜"
},
"actions": {
"playNow": "바로 재생"
}
},
"share": {
"name": "공유",
"fields": {
"username": "공유자",
"url": "URL",
"description": "설명",
"contents": "컨텐츠",
"expiresAt": "만료 날짜",
"lastVisitedAt": "최근 방문",
"visitCount": "방문 수",
"format": "포맷",
"maxBitRate": "최대 비트레이트",
"updatedAt": "업데이트 날짜",
"createdAt": "생성 날짜",
"downloadable": ""
}
}
},
"ra": {
"auth": {
"welcome1": "Navidrome을 설치해 주셔서 감사합니다!",
"welcome2": "관리자 사용자를 생성하고 시작해 보세요",
"confirmPassword": "비밀번호 확인",
"buttonCreateAdmin": "관리자 생성",
"auth_check_error": "인증에 실패했습니다. 다시 로그인하세요",
"user_menu": "프로필",
"username": "사용자명",
"password": "비밀번호",
"sign_in": "로그인",
"sign_in_error": "인증에 실패했습니다. 입력값을 확인하세요",
"logout": "로그아웃"
},
"validation": {
"invalidChars": "문자와 숫자만 사용하세요",
"passwordDoesNotMatch": "비밀번호가 일치하지 않습니다",
"required": "필수 항목입니다",
"minLength": "%{min}자 이상이어야 합니다",
"maxLength": "%{max}자 이하이어야 합니다",
"minValue": "%{min} 이상이어야 합니다",
"maxValue": "%{max} 이하이어야 합니다",
"number": "숫자여야 합니다",
"email": "유효한 이메일 주소여야 합니다",
"oneOf": "다음 중 하나여야 합니다: %{options}",
"regex": "다음과 같은 형식이어야 합니다: %{pattern}",
"unique": "고유해야 합니다",
"url": "유효한 URL을 입력하세요"
},
"action": {
"add_filter": "필터 추가",
"add": "추가",
"back": "뒤로",
"bulk_actions": "%{smart_count}개 선택",
"cancel": "취소",
"clear_input_value": "비우기",
"clone": "복제",
"confirm": "확인",
"create": "생성",
"delete": "삭제",
"edit": "편집",
"export": "내보내기",
"list": "목록",
"refresh": "새로고침",
"remove_filter": "필터 삭제",
"remove": "삭제",
"save": "저장",
"search": "검색",
"show": "상세 정보",
"sort": "정렬",
"undo": "실행 취소",
"expand": "확장",
"close": "닫기",
"open_menu": "메뉴 열기",
"close_menu": "메뉴 닫기",
"unselect": "선택 해제",
"skip": "스킵",
"bulk_actions_mobile": "1 |||| %{smart_count}",
"share": "공유",
"download": "다운로드"
},
"boolean": {
"true": "예",
"false": "아니요"
},
"page": {
"create": "%{name} 생성",
"dashboard": "대시보드",
"edit": "%{name} #%{id}",
"error": "문제가 발생했습니다",
"list": "%{name}",
"loading": "로딩 중입니다. 잠시 기다려주세요",
"not_found": "찾을 수 없습니다",
"show": "%{name} #%{id}",
"empty": "%{name}이(가) 없습니다",
"invite": "생성하시겠습니까?"
},
"input": {
"file": {
"upload_several": "파일을 끌어 놓거나 클릭하여 업로드하세요",
"upload_single": "파일을 끌어 놓거나 클릭하여 업로드하세요"
},
"image": {
"upload_several": "이미지를 끌어 놓거나 클릭하여 업로드하세요",
"upload_single": "이미지를 끌어 놓거나 클릭하여 업로드하세요"
},
"references": {
"all_missing": "사용 가능한 데이터가 없습니다",
"many_missing": "선택한 데이터 중 일부가 사용 가능하지 않습니다",
"single_missing": "선택한 데이터가 사용 가능하지 않습니다"
},
"password": {
"toggle_visible": "숨기기",
"toggle_hidden": "보이기"
}
},
"message": {
"about": "정보",
"are_you_sure": "정말로 이 작업을 수행하시겠습니까?",
"bulk_delete_content": "%{name}을(를) 삭제하시겠습니까? |||| %{smart_count}개의 항목을 삭제하시겠습니까?",
"bulk_delete_title": "%{name} 삭제 |||| %{name} %{smart_count}개 삭제",
"delete_content": "삭제하시겠습니까?",
"delete_title": "%{name} #%{id} 삭제",
"details": "세부 정보",
"error": "클라이언트 오류로 처리를 완료할 수 없습니다",
"invalid_form": "입력값에 오류가 있습니다. 오류 메시지를 확인하세요",
"loading": "로딩 중입니다. 잠시만 기다려주세요",
"no": "아니요",
"not_found": "잘못된 URL을 입력하거나 잘못된 링크를 따라갔습니다",
"yes": "예",
"unsaved_changes": "변경 사항이 저장되지 않았습니다. 이 페이지를 떠나시겠습니까?"
},
"navigation": {
"no_results": "결과가 없습니다",
"no_more_results": "페이지 %{page}는 최대 페이지 수를 초과했습니다. 이전 페이지로 돌아가세요",
"page_out_of_boundaries": "페이지 %{page}는 최대 페이지 수를 초과했습니다",
"page_out_from_end": "마지막 페이지 이후로 이동할 수 없습니다",
"page_out_from_begin": "첫 페이지 이전으로 이동할 수 없습니다",
"page_range_info": "%{offsetBegin}-%{offsetEnd} / %{total}",
"page_rows_per_page": "페이지당 항목 수:",
"next": "다음",
"prev": "이전",
"skip_nav": "메뉴 건너뛰기"
},
"notification": {
"updated": "업데이트되었습니다 |||| %{smart_count}개 업데이트되었습니다",
"created": "생성되었습니다",
"deleted": "삭제되었습니다 |||| %{smart_count}개 삭제되었습니다",
"bad_item": "잘못된 항목입니다",
"item_doesnt_exist": "항목이 존재하지 않습니다",
"http_error": "통신 오류가 발생했습니다",
"data_provider_error": "dataProvider 오류입니다. 자세한 내용은 콘솔을 확인하세요",
"i18n_error": "번역 파일을 로드할 수 없습니다",
"canceled": "취소되었습니다",
"logged_out": "인증에 실패했습니다. 다시 로그인하세요",
"new_version": "새로운 버전이 사용 가능합니다! 페이지를 새로 고쳐주세요."
},
"toggleFieldsMenu": {
"columnsToDisplay": "표시 열",
"layout": "레이아웃",
"grid": "그리드",
"table": "테이블"
}
},
"message": {
"note": "주의",
"transcodingDisabled": "보안상의 이유로 웹 인터페이스에서 트랜스코드 설정이 비활성화되어 있습니다.\n이를 설정하려면 환경 변수 %{config}를 설정하고 서버를 재시작하십시오.",
"transcodingEnabled": "Navidrome은 현재 %{config} 설정으로 실행되며, 웹 인터페이스의 트랜스코드 설정에 따라 명령을 실행할 수 있습니다.\n보안상의 이유로 이 설정은 트랜스코드 설정을 변경할 때만 활성화하는 것을 권장합니다.",
"songsAddedToPlaylist": "플레이리스트에 1곡 추가되었습니다 |||| 플레이리스트에 %{smart_count}곡 추가되었습니다",
"noPlaylistsAvailable": "사용 가능하지 않음",
"delete_user_title": "'%{name}' 삭제",
"delete_user_content": "이 사용자와 그의 모든 데이터(플레이리스트 및 설정 등)를 삭제하시겠습니까?",
"notifications_blocked": "브라우저의 설정으로 이 사이트의 알림이 차단되어 있습니다",
"notifications_not_available": "이 브라우저는 데스크톱 알림을 지원하지 않습니다",
"lastfmLinkSuccess": "Last.fm과 연결되어 scrobble이 활성화되었습니다",
"lastfmLinkFailure": "Last.fm과 연결할 수 없습니다",
"lastfmUnlinkSuccess": "설정이 해제되어 Last.fm으로의 scrobble이 비활성화되었습니다",
"lastfmUnlinkFailure": "Last.fm과 연결 해제를 실패했습니다",
"openIn": {
"lastfm": "Last.fm에서 열기",
"musicbrainz": "MusicBrainz에서 열기"
},
"lastfmLink": "계속 읽기",
"listenBrainzLinkSuccess": "%{user}에 대한 scrobbling 설정이 성공적으로 완료되었습니다",
"listenBrainzLinkFailure": "ListenBrainz와 연결에 실패했습니다: %{error}",
"listenBrainzUnlinkSuccess": "ListenBrainz와의 연결과 scrobbling이 비활성화되었습니다",
"listenBrainzUnlinkFailure": "ListenBrainz와의 연결 해제를 실패했습니다",
"downloadOriginalFormat": "원본 형식으로 다운로드",
"shareOriginalFormat": "원본 형식으로 공유",
"shareDialogTitle": "%{resource} '%{name}' 공유",
"shareBatchDialogTitle": "1 %{resource} 공유 |||| %{smart_count} %{resource} 공유",
"shareSuccess": "복사되었습니다: %{url}",
"shareFailure": "복사하지 못했습니다 %{url}",
"downloadDialogTitle": "다운로드 %{resource} '%{name}' (%{size})",
"shareCopyToClipboard": ""
},
"menu": {
"library": "라이브러리",
"settings": "설정",
"version": "버전",
"theme": "테마",
"personal": {
"name": "개인 설정",
"options": {
"theme": "테마",
"language": "언어",
"defaultView": "기본 뷰",
"desktop_notifications": "데스크톱 알림",
"lastfmScrobbling": "Last.fm으로 scrobble하기",
"listenBrainzScrobbling": "ListenBrainz로 scrobble하기",
"replaygain": "ReplayGain 모드",
"preAmp": "프리앰프",
"gain": {
"none": "비활성화",
"album": "앨범 Gain 사용",
"track": "트랙 Gain 사용"
}
}
},
"albumList": "앨범",
"about": "상세 정보",
"playlists": "플레이리스트",
"sharedPlaylists": "공유된 플레이리스트"
},
"player": {
"playListsText": "재생 목록",
"openText": "열기",
"closeText": "닫기",
"notContentText": "음악이 없습니다",
"clickToPlayText": "클릭하여 재생",
"clickToPauseText": "일시 정지",
"nextTrackText": "다음 곡",
"previousTrackText": "이전 곡",
"reloadText": "새로 고침",
"volumeText": "음량",
"toggleLyricText": "가사 전환",
"toggleMiniModeText": "최소화",
"destroyText": "삭제",
"downloadText": "다운로드",
"removeAudioListsText": "목록 비우기",
"clickToDeleteText": "클릭하여 %{name} 삭제",
"emptyLyricText": "가사가 없습니다",
"playModeText": {
"order": "순서대로",
"orderLoop": "반복",
"singleLoop": "한 곡 반복",
"shufflePlay": "셔플"
}
},
"about": {
"links": {
"homepage": "홈페이지",
"source": "소스 코드",
"featureRequests": "기능 요청"
}
},
"activity": {
"title": "활동",
"totalScanned": "스캔된 폴더",
"quickScan": "빠른 스캔",
"fullScan": "전체 스캔",
"serverUptime": "서버 가동 시간",
"serverDown": "서버 오프라인"
},
"help": {
"title": "Navidrome 단축키",
"hotkeys": {
"show_help": "도움말 표시",
"toggle_menu": "사이드바 표시/숨기기",
"toggle_play": "재생/정지",
"prev_song": "이전 곡",
"next_song": "다음 곡",
"vol_up": "음량 높이기",
"vol_down": "음량 낮추기",
"toggle_love": "별표 토글",
"current_song": "현재 곡으로 이동"
}
}
}

View File

@@ -54,7 +54,11 @@
"comment": "Commentaar",
"rating": "Beoordeling",
"createdAt": "Datum toegevoegd",
"size": "Grootte"
"size": "Grootte",
"originalDate": "Origineel",
"releaseDate": "Uitgegeven",
"releases": "Uitgave |||| Uitgaven",
"released": "Uitgegeven"
},
"actions": {
"playAll": "Afspelen",

View File

@@ -54,7 +54,11 @@
"comment": "Komentarz",
"rating": "Ocena",
"createdAt": "Data dodania",
"size": "Rozmiar"
"size": "Rozmiar",
"originalDate": "Pierwotna Data",
"releaseDate": "Data Wydania",
"releases": "Wydanie |||| Wydania",
"released": "Wydany"
},
"actions": {
"playAll": "Odtwarzaj",

View File

@@ -54,7 +54,11 @@
"comment": "Comentário",
"rating": "Classificação",
"createdAt": "Adicionado em",
"size": "Tamanho"
"size": "Tamanho",
"originalDate": "Original",
"releaseDate": "Data de Lançamento",
"releases": "Versão||||Versões",
"released": "Lançado"
},
"actions": {
"playAll": "Tocar",

View File

@@ -2,7 +2,7 @@
"languageName": "Pусский",
"resources": {
"song": {
"name": "Трек |||| Треки",
"name": "Трек |||| Треки |||| Треков",
"fields": {
"albumArtist": "Исполнитель альбома",
"duration": "Длительность",
@@ -54,7 +54,11 @@
"comment": "Комментарий",
"rating": "Рейтинг",
"createdAt": "Дата добавления",
"size": "Размер"
"size": "Размер",
"originalDate": "Оригинал",
"releaseDate": "Релиз",
"releases": "Релиз |||| Релиза |||| Релизов",
"released": "Релиз"
},
"actions": {
"playAll": "Играть",

View File

@@ -54,7 +54,11 @@
"comment": "Opomba",
"rating": "Ocena",
"createdAt": "Datum dodano",
"size": "Velikost"
"size": "Velikost",
"originalDate": "Original",
"releaseDate": "Izdano",
"releases": "Izdaja |||| Izdaje",
"released": "Izdano"
},
"actions": {
"playAll": "Predvajaj vse",
@@ -191,7 +195,7 @@
"maxBitRate": "Maks. bitna hitrost",
"updatedAt": "Posodobljeno ob",
"createdAt": "Ustvarjeno ob",
"downloadable": ""
"downloadable": "Dovoli prenose?"
}
}
},
@@ -369,7 +373,7 @@
"shareSuccess": "URL kopiran v odložišče: %{url}",
"shareFailure": "Napaka pri kopiranju URL-ja %{url} v odložišče",
"downloadDialogTitle": "Prenesi %{resource} '%{name}' (%{size})",
"shareCopyToClipboard": ""
"shareCopyToClipboard": "Kopiraj v odložišče: Ctrl+C, Enter"
},
"menu": {
"library": "Knjižnica",

View File

@@ -54,7 +54,11 @@
"comment": "Kommentar",
"rating": "Betyg",
"createdAt": "",
"size": ""
"size": "",
"originalDate": "",
"releaseDate": "",
"releases": "",
"released": ""
},
"actions": {
"playAll": "Spela",

View File

@@ -54,7 +54,11 @@
"comment": "ความคิดเห็น",
"rating": "Rating",
"createdAt": "",
"size": ""
"size": "",
"originalDate": "",
"releaseDate": "",
"releases": "",
"released": ""
},
"actions": {
"playAll": "เล่นทั้งหมด",

View File

@@ -54,7 +54,11 @@
"comment": "",
"rating": "",
"createdAt": "",
"size": ""
"size": "",
"originalDate": "",
"releaseDate": "",
"releases": "",
"released": ""
},
"actions": {
"playAll": "Çaldır",

View File

@@ -26,7 +26,7 @@
"bpm": "Темп",
"playDate": "Востаннє відтворено",
"channels": "Канали",
"createdAt": ""
"createdAt": "Додано"
},
"actions": {
"addToQueue": "Прослухати пізніше",
@@ -53,8 +53,12 @@
"updatedAt": "Оновлено",
"comment": "Коментар",
"rating": "Рейтинг",
"createdAt": "",
"size": ""
"createdAt": "Додано",
"size": "Розмір",
"originalDate": "",
"releaseDate": "",
"releases": "",
"released": ""
},
"actions": {
"playAll": "Прослухати",
@@ -64,7 +68,7 @@
"addToPlaylist": "Додати у список відтворення",
"download": "Завантажити",
"info": "Отримати інформацію",
"share": ""
"share": "Поширити"
},
"lists": {
"all": "Усі",
@@ -85,7 +89,7 @@
"playCount": "Відтворено",
"rating": "Рейтинг",
"genre": "Жанр",
"size": ""
"size": "Розмір"
}
},
"user": {
@@ -101,7 +105,7 @@
"changePassword": "Змінити пароль?",
"currentPassword": "Поточний пароль",
"newPassword": "Новий пароль",
"token": ""
"token": "Токен"
},
"helperTexts": {
"name": "Змінене ім'я буде відображатися при наступній авторизації"
@@ -112,8 +116,8 @@
"deleted": "Користувач видалений"
},
"message": {
"listenBrainzToken": "",
"clickHereForToken": ""
"listenBrainzToken": "Введіть свій токен користувача ListenBrainz.",
"clickHereForToken": "Натисніть тут для отримання токену"
}
},
"player": {
@@ -126,7 +130,7 @@
"userName": "Iмя користувача",
"lastSeen": "Останній візит о",
"reportRealPath": "Повідомте про реальний шлях",
"scrobbleEnabled": ""
"scrobbleEnabled": "Надсилайте Scrobbles до зовнішніх сервісів"
}
},
"transcoding": {
@@ -156,8 +160,8 @@
"selectPlaylist": "Вибрати список відтворення:",
"addNewPlaylist": "Створити \"%{name}\"",
"export": "Експортувати",
"makePublic": "",
"makePrivate": ""
"makePublic": "Зробити публічним",
"makePrivate": "Зробити приватним"
},
"message": {
"duplicate_song": "Додати повторювані пісні",
@@ -165,33 +169,33 @@
}
},
"radio": {
"name": "",
"name": "Радіо |||| Радіо",
"fields": {
"name": "",
"streamUrl": "",
"homePageUrl": "",
"updatedAt": "",
"createdAt": ""
"name": "Назва",
"streamUrl": "Посилання на стрім",
"homePageUrl": "Посилання на домашню сторінку",
"updatedAt": "Оновлено",
"createdAt": "Створено"
},
"actions": {
"playNow": ""
"playNow": "Зараз грає"
}
},
"share": {
"name": "",
"name": "Поширити |||| Поширення",
"fields": {
"username": "",
"url": "",
"description": "",
"contents": "",
"expiresAt": "",
"lastVisitedAt": "",
"visitCount": "",
"format": "",
"maxBitRate": "",
"updatedAt": "",
"createdAt": "",
"downloadable": ""
"username": "Поширено",
"url": "Посилання",
"description": "Опис",
"contents": "Вміст",
"expiresAt": "Дійсний",
"lastVisitedAt": "Останній візит",
"visitCount": "Відвідин",
"format": "Формат",
"maxBitRate": "Макс. Біт рейт",
"updatedAt": "Оновлено",
"createdAt": "Створено",
"downloadable": "Дозволити завантаження?"
}
}
},
@@ -222,7 +226,7 @@
"oneOf": "Повинен бути одним з: %{options}",
"regex": "Повинен відповідати формату (регулярний вираз): %{pattern}",
"unique": "Має бути унікальним",
"url": ""
"url": "Повинно бути дійсне посилання"
},
"action": {
"add_filter": "Додати фільтр",
@@ -252,9 +256,9 @@
"close_menu": "Закрити меню",
"unselect": "Забрати виділення",
"skip": "Пропустити",
"bulk_actions_mobile": "",
"share": "",
"download": ""
"bulk_actions_mobile": "1 |||| %{smart_count}",
"share": "Поширити",
"download": "Завантаження"
},
"boolean": {
"true": "Так",
@@ -349,27 +353,27 @@
"delete_user_content": "Ви справді хочете видалити цього користувача і усі його данні (включаючи списки відтворення і налаштування)?",
"notifications_blocked": "У вас заблоковані Сповіщення для цього сайту у вашому браузері",
"notifications_not_available": "Ваш браузер не підтримує сповіщень або доступ до Navidrome не використовує https",
"lastfmLinkSuccess": "",
"lastfmLinkFailure": "",
"lastfmUnlinkSuccess": "",
"lastfmUnlinkFailure": "",
"lastfmLinkSuccess": "Last.fm успішно підключено, scrobbling увімкнено",
"lastfmLinkFailure": "Last.fm не вдалося підключити",
"lastfmUnlinkSuccess": "Last.fm від'єднано та вимкнено scrobbling",
"lastfmUnlinkFailure": "Last.fm не вдалося від'єднати",
"openIn": {
"lastfm": "Відкрити у Last.fm",
"musicbrainz": "Відкрити у MusicBrainz"
},
"lastfmLink": "Читати більше...",
"listenBrainzLinkSuccess": "",
"listenBrainzLinkFailure": "",
"listenBrainzUnlinkSuccess": "",
"listenBrainzUnlinkFailure": "",
"downloadOriginalFormat": "",
"shareOriginalFormat": "",
"shareDialogTitle": "",
"shareBatchDialogTitle": "",
"shareSuccess": "",
"shareFailure": "",
"downloadDialogTitle": "",
"shareCopyToClipboard": ""
"listenBrainzLinkSuccess": "ListenBrainz успішно підключено і scrobbling увімкнено для користувача: %{user}.",
"listenBrainzLinkFailure": "ListenBrainz не вдалося зв'язати: %{error}",
"listenBrainzUnlinkSuccess": "ListenBrainz від'єднано та вимкнено scrobbling",
"listenBrainzUnlinkFailure": "ListenBrainz не вдалося від'єднати",
"downloadOriginalFormat": "Завантажити в вихідному форматі",
"shareOriginalFormat": "Поширити у вихідному форматі",
"shareDialogTitle": "Поширити %{resource} '%{name}'",
"shareBatchDialogTitle": "Поширити 1 %{resource} |||| Поширити %{smart_count} %{resource}",
"shareSuccess": "URL скопійований в буфер обміну: %{url}",
"shareFailure": "Помилка копіюваня URL %{url} в буфер обміну",
"downloadDialogTitle": "Завантаження %{resource} '%{name}' (%{size})",
"shareCopyToClipboard": "Скопіювати в буфер: Ctrl+C, Enter"
},
"menu": {
"library": "Бібліотека",
@@ -383,14 +387,14 @@
"language": "Мова",
"defaultView": "Вигляд по замовчуванню",
"desktop_notifications": "Сповіщення",
"lastfmScrobbling": "",
"listenBrainzScrobbling": "",
"replaygain": "",
"preAmp": "",
"lastfmScrobbling": "Scrobble на Last.fm",
"listenBrainzScrobbling": "Scrobble на ListenBrainz",
"replaygain": "Режим ReplayGain",
"preAmp": "ReplayGain підсилення (дБ)",
"gain": {
"none": "",
"album": "",
"track": ""
"none": "Вимкнено",
"album": "Використовуйте підсилення для Альбому",
"track": "Використовуйте посилення доріжки"
}
}
},
@@ -450,7 +454,7 @@
"vol_up": "Гучність вгору",
"vol_down": "Гучність вниз",
"toggle_love": "Відмітити поточні пісні",
"current_song": ""
"current_song": "Перейти до поточної пісні"
}
}
}

View File

@@ -2,7 +2,7 @@
"languageName": "简体中文",
"resources": {
"song": {
"name": "歌曲 |||| 歌曲",
"name": "歌曲",
"fields": {
"albumArtist": "专辑歌手",
"duration": "时长",
@@ -39,7 +39,7 @@
}
},
"album": {
"name": "专辑 |||| 专辑",
"name": "专辑",
"fields": {
"albumArtist": "专辑歌手",
"artist": "歌手",
@@ -54,7 +54,11 @@
"comment": "注释",
"rating": "评分",
"createdAt": "创建于",
"size": "文件大小"
"size": "文件大小",
"originalDate": "",
"releaseDate": "",
"releases": "",
"released": ""
},
"actions": {
"playAll": "立即播放",
@@ -77,7 +81,7 @@
}
},
"artist": {
"name": "艺术家 |||| 艺术家",
"name": "艺术家",
"fields": {
"name": "名称",
"albumCount": "专辑数",
@@ -89,7 +93,7 @@
}
},
"user": {
"name": "用户 |||| 用户",
"name": "用户",
"fields": {
"userName": "用户名",
"isAdmin": "是否管理员",
@@ -117,7 +121,7 @@
}
},
"player": {
"name": "客户端 |||| 客户端",
"name": "客户端",
"fields": {
"name": "名称",
"transcodingId": "转码编号",
@@ -130,7 +134,7 @@
}
},
"transcoding": {
"name": "转码 |||| 转码",
"name": "转码",
"fields": {
"name": "名称",
"targetFormat": "目标格式",
@@ -139,7 +143,7 @@
}
},
"playlist": {
"name": "歌单 |||| 歌单",
"name": "歌单",
"fields": {
"name": "名称",
"duration": "时长",
@@ -165,7 +169,7 @@
}
},
"radio": {
"name": "电台 |||| 电台",
"name": "电台",
"fields": {
"name": "名称",
"streamUrl": "推流地址",
@@ -178,7 +182,7 @@
}
},
"share": {
"name": "分享 |||| 分享",
"name": "分享",
"fields": {
"username": "分享者",
"url": "链接",
@@ -252,7 +256,7 @@
"close_menu": "关闭菜单",
"unselect": "未选择",
"skip": "跳过",
"bulk_actions_mobile": "1 |||| %{smart_count}",
"bulk_actions_mobile": "%{smart_count}",
"share": "分享",
"download": "下载"
},
@@ -294,8 +298,8 @@
"message": {
"about": "关于",
"are_you_sure": "您确定要进行此操作?",
"bulk_delete_content": "您确定要删除 %{name} |||| 您确定要删除 %{smart_count} 项?",
"bulk_delete_title": "删除 %{name} |||| 删除 %{smart_count} 项 %{name}",
"bulk_delete_content": "您确定要删除 %{smart_count} 项 %{name}",
"bulk_delete_title": "删除 %{smart_count} 项 %{name}",
"delete_content": "您确定要删除该条目?",
"delete_title": "删除 %{name} #%{id}",
"details": "详情",
@@ -309,8 +313,8 @@
},
"navigation": {
"no_results": "无内容",
"no_more_results": "页码 %{page} 超出边界,尝试返回上一页",
"page_out_of_boundaries": "页码 %{page} 超出边界",
"no_more_results": "页码 %{page} 超出范围,尝试返回上一页",
"page_out_of_boundaries": "页码 %{page} 超出范围",
"page_out_from_end": "已经最后一页",
"page_out_from_begin": "已经是第一页",
"page_range_info": "%{offsetBegin}-%{offsetEnd} / %{total}",
@@ -320,11 +324,11 @@
"skip_nav": "跳过"
},
"notification": {
"updated": "已更新 1 项 |||| 已更新 %{smart_count} 项",
"updated": "已更新 %{smart_count} 项",
"created": "已新建 1 项",
"deleted": "已删除 1 项 |||| 已删除 %{smart_count} 项",
"deleted": "已删除 %{smart_count} 项",
"bad_item": "不正确的项",
"item_doesnt_exist": "项不存在",
"item_doesnt_exist": "项不存在",
"http_error": "与服务通信出错",
"data_provider_error": "数据来源错误,请检查控制台的详细信息",
"i18n_error": "加载所选语言时出错",
@@ -343,7 +347,7 @@
"note": "说明",
"transcodingDisabled": "出于安全原因,从 Web 界面更改转码配置的功能已被禁用。要更改(编辑或新增)转码选项,请在启用 %{config} 选项的情况下重新启动服务器。",
"transcodingEnabled": "Navidrome 当前与 %{config} 一起使用,可以通过配置转码选项来执行任意命令,建议仅在配置转码选项时启用此功能。",
"songsAddedToPlaylist": "已添加 1 首歌到歌单 |||| 已添加 %{smart_count} 首歌到歌单",
"songsAddedToPlaylist": "已添加 %{smart_count} 首歌到歌单",
"noPlaylistsAvailable": "没有有效的歌单",
"delete_user_title": "删除用户 %{name}",
"delete_user_content": "您确定要删除该用户及其相关数据(包括歌单和用户配置)吗?",
@@ -365,7 +369,7 @@
"downloadOriginalFormat": "下载原始格式",
"shareOriginalFormat": "分享原始格式",
"shareDialogTitle": "分享 %{resource} '%{name}'",
"shareBatchDialogTitle": "分享 1 个 %{resource} |||| 分享 %{smart_count} 个 %{resource}",
"shareBatchDialogTitle": "分享 %{smart_count} 个 %{resource}",
"shareSuccess": "分享链接已复制: %{url}",
"shareFailure": "分享链接复制失败: %{url}",
"downloadDialogTitle": "下载 %{resource} '%{name}' (%{size})",

View File

@@ -54,7 +54,11 @@
"comment": "註釋",
"rating": "評分",
"createdAt": "",
"size": ""
"size": "",
"originalDate": "",
"releaseDate": "",
"releases": "",
"released": ""
},
"actions": {
"playAll": "立即播放",

View File

@@ -6,7 +6,6 @@ import (
"os"
"path/filepath"
"strings"
"time"
"github.com/deluan/sanitize"
"github.com/navidrome/navidrome/conf"
@@ -62,7 +61,7 @@ func (s mediaFileMapper) toMediaFile(md metadata.Tags) model.MediaFile {
mf.OrderArtistName = sanitizeFieldForSorting(mf.Artist)
mf.OrderAlbumArtistName = sanitizeFieldForSorting(mf.AlbumArtist)
mf.CatalogNum = md.CatalogNum()
mf.MbzTrackID = md.MbzTrackID()
mf.MbzRecordingID = md.MbzRecordingID()
mf.MbzReleaseTrackID = md.MbzReleaseTrackID()
mf.MbzAlbumID = md.MbzAlbumID()
mf.MbzArtistID = md.MbzArtistID()
@@ -76,7 +75,7 @@ func (s mediaFileMapper) toMediaFile(md metadata.Tags) model.MediaFile {
mf.Comment = utils.SanitizeText(md.Comment())
mf.Lyrics = utils.SanitizeText(md.Lyrics())
mf.Bpm = md.Bpm()
mf.CreatedAt = time.Now()
mf.CreatedAt = md.BirthTime()
mf.UpdatedAt = md.ModificationTime()
return *mf
@@ -187,5 +186,13 @@ func (s mediaFileMapper) mapDates(md metadata.Tags) (int, string, int, string, i
if taggedLikePicard {
return originalYear, originalDate, originalYear, originalDate, year, date
}
// when there's no Date, first fall back to Original Date, then to Release Date.
if year == 0 {
if originalYear > 0 {
year, date = originalYear, originalDate
} else {
year, date = releaseYear, releaseDate
}
}
return year, date, originalYear, originalDate, releaseYear, releaseDate
}

View File

@@ -14,10 +14,10 @@ import (
var _ = Describe("mapping", func() {
Describe("mediaFileMapper", func() {
var mapper *mediaFileMapper
BeforeEach(func() {
mapper = newMediaFileMapper("/music", nil)
})
Describe("mapTrackTitle", func() {
BeforeEach(func() {
mapper = newMediaFileMapper("/music", nil)
})
It("returns the Title when it is available", func() {
md := metadata.NewTag("/music/artist/album01/Song.mp3", nil, metadata.ParsedTags{"title": []string{"This is not a love song"}})
Expect(mapper.mapTrackTitle(md)).To(Equal("This is not a love song"))
@@ -27,7 +27,141 @@ var _ = Describe("mapping", func() {
Expect(mapper.mapTrackTitle(md)).To(Equal("artist/album01/Song"))
})
})
Describe("mapGenres", func() {
var gr model.GenreRepository
var ctx context.Context
BeforeEach(func() {
ctx = context.Background()
ds := &tests.MockDataStore{}
gr = ds.Genre(ctx)
gr = newCachedGenreRepository(ctx, gr)
mapper = newMediaFileMapper("/", gr)
})
It("returns empty if no genres are available", func() {
g, gs := mapper.mapGenres(nil)
Expect(g).To(BeEmpty())
Expect(gs).To(BeEmpty())
})
It("returns genres", func() {
g, gs := mapper.mapGenres([]string{"Rock", "Electronic"})
Expect(g).To(Equal("Rock"))
Expect(gs).To(HaveLen(2))
Expect(gs[0].Name).To(Equal("Rock"))
Expect(gs[1].Name).To(Equal("Electronic"))
})
It("parses multi-valued genres", func() {
g, gs := mapper.mapGenres([]string{"Rock;Dance", "Electronic", "Rock"})
Expect(g).To(Equal("Rock"))
Expect(gs).To(HaveLen(3))
Expect(gs[0].Name).To(Equal("Rock"))
Expect(gs[1].Name).To(Equal("Dance"))
Expect(gs[2].Name).To(Equal("Electronic"))
})
It("trims genres names", func() {
_, gs := mapper.mapGenres([]string{"Rock ; Dance", " Electronic "})
Expect(gs).To(HaveLen(3))
Expect(gs[0].Name).To(Equal("Rock"))
Expect(gs[1].Name).To(Equal("Dance"))
Expect(gs[2].Name).To(Equal("Electronic"))
})
It("does not break on spaces", func() {
_, gs := mapper.mapGenres([]string{"New Wave"})
Expect(gs).To(HaveLen(1))
Expect(gs[0].Name).To(Equal("New Wave"))
})
})
Describe("mapDates", func() {
var md metadata.Tags
BeforeEach(func() {
mapper = newMediaFileMapper("/", nil)
})
Context("when all date fields are provided", func() {
BeforeEach(func() {
md = metadata.NewTag("/music/artist/album01/Song.mp3", nil, metadata.ParsedTags{
"date": []string{"2023-03-01"},
"originaldate": []string{"2022-05-10"},
"releasedate": []string{"2023-01-15"},
})
})
It("should map all date fields correctly", func() {
year, date, originalYear, originalDate, releaseYear, releaseDate := mapper.mapDates(md)
Expect(year).To(Equal(2023))
Expect(date).To(Equal("2023-03-01"))
Expect(originalYear).To(Equal(2022))
Expect(originalDate).To(Equal("2022-05-10"))
Expect(releaseYear).To(Equal(2023))
Expect(releaseDate).To(Equal("2023-01-15"))
})
})
Context("when date field is missing", func() {
BeforeEach(func() {
md = metadata.NewTag("/music/artist/album01/Song.mp3", nil, metadata.ParsedTags{
"originaldate": []string{"2022-05-10"},
"releasedate": []string{"2023-01-15"},
})
})
It("should fallback to original date if date is missing", func() {
year, date, _, _, _, _ := mapper.mapDates(md)
Expect(year).To(Equal(2022))
Expect(date).To(Equal("2022-05-10"))
})
})
Context("when original and release dates are missing", func() {
BeforeEach(func() {
md = metadata.NewTag("/music/artist/album01/Song.mp3", nil, metadata.ParsedTags{
"date": []string{"2023-03-01"},
})
})
It("should only map the date field", func() {
year, date, originalYear, originalDate, releaseYear, releaseDate := mapper.mapDates(md)
Expect(year).To(Equal(2023))
Expect(date).To(Equal("2023-03-01"))
Expect(originalYear).To(BeZero())
Expect(originalDate).To(BeEmpty())
Expect(releaseYear).To(BeZero())
Expect(releaseDate).To(BeEmpty())
})
})
Context("when date fields are in an incorrect format", func() {
BeforeEach(func() {
md = metadata.NewTag("/music/artist/album01/Song.mp3", nil, metadata.ParsedTags{
"date": []string{"invalid-date"},
})
})
It("should handle invalid date formats gracefully", func() {
year, date, _, _, _, _ := mapper.mapDates(md)
Expect(year).To(BeZero())
Expect(date).To(BeEmpty())
})
})
Context("when all date fields are missing", func() {
It("should return zero values for all date fields", func() {
year, date, originalYear, originalDate, releaseYear, releaseDate := mapper.mapDates(md)
Expect(year).To(BeZero())
Expect(date).To(BeEmpty())
Expect(originalYear).To(BeZero())
Expect(originalDate).To(BeEmpty())
Expect(releaseYear).To(BeZero())
Expect(releaseDate).To(BeEmpty())
})
})
})
})
Describe("sanitizeFieldForSorting", func() {
BeforeEach(func() {
conf.Server.IgnoredArticles = "The O"
@@ -42,52 +176,4 @@ var _ = Describe("mapping", func() {
Expect(sanitizeFieldForSorting("Õ Blésq Blom")).To(Equal("Blesq Blom"))
})
})
Describe("mapGenres", func() {
var mapper *mediaFileMapper
var gr model.GenreRepository
var ctx context.Context
BeforeEach(func() {
ctx = context.Background()
ds := &tests.MockDataStore{}
gr = ds.Genre(ctx)
gr = newCachedGenreRepository(ctx, gr)
mapper = newMediaFileMapper("/", gr)
})
It("returns empty if no genres are available", func() {
g, gs := mapper.mapGenres(nil)
Expect(g).To(BeEmpty())
Expect(gs).To(BeEmpty())
})
It("returns genres", func() {
g, gs := mapper.mapGenres([]string{"Rock", "Electronic"})
Expect(g).To(Equal("Rock"))
Expect(gs).To(HaveLen(2))
Expect(gs[0].Name).To(Equal("Rock"))
Expect(gs[1].Name).To(Equal("Electronic"))
})
It("parses multi-valued genres", func() {
g, gs := mapper.mapGenres([]string{"Rock;Dance", "Electronic", "Rock"})
Expect(g).To(Equal("Rock"))
Expect(gs).To(HaveLen(3))
Expect(gs[0].Name).To(Equal("Rock"))
Expect(gs[1].Name).To(Equal("Dance"))
Expect(gs[2].Name).To(Equal("Electronic"))
})
It("trims genres names", func() {
_, gs := mapper.mapGenres([]string{"Rock ; Dance", " Electronic "})
Expect(gs).To(HaveLen(3))
Expect(gs[0].Name).To(Equal("Rock"))
Expect(gs[1].Name).To(Equal("Dance"))
Expect(gs[2].Name).To(Equal("Electronic"))
})
It("does not break on spaces", func() {
_, gs := mapper.mapGenres([]string{"New Wave"})
Expect(gs).To(HaveLen(1))
Expect(gs[0].Name).To(Equal("New Wave"))
})
})
})

View File

@@ -10,6 +10,7 @@ import (
"strings"
"time"
"github.com/djherbis/times"
"github.com/google/uuid"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
@@ -103,10 +104,12 @@ func (t Tags) Date() (int, string) { return t.getDate("date") }
func (t Tags) OriginalDate() (int, string) { return t.getDate("originaldate") }
func (t Tags) ReleaseDate() (int, string) { return t.getDate("releasedate") }
func (t Tags) Comment() string { return t.getFirstTagValue("comment") }
func (t Tags) Lyrics() string { return t.getFirstTagValue("lyrics", "lyrics-eng") }
func (t Tags) Compilation() bool { return t.getBool("tcmp", "compilation") }
func (t Tags) TrackNumber() (int, int) { return t.getTuple("track", "tracknumber") }
func (t Tags) DiscNumber() (int, int) { return t.getTuple("disc", "discnumber") }
func (t Tags) Lyrics() string {
return t.getFirstTagValue("lyrics", "lyrics-eng", "unsynced_lyrics", "unsynced lyrics", "unsyncedlyrics")
}
func (t Tags) Compilation() bool { return t.getBool("tcmp", "compilation") }
func (t Tags) TrackNumber() (int, int) { return t.getTuple("track", "tracknumber") }
func (t Tags) DiscNumber() (int, int) { return t.getTuple("disc", "discnumber") }
func (t Tags) DiscSubtitle() string {
return t.getFirstTagValue("tsst", "discsubtitle", "setsubtitle")
}
@@ -120,7 +123,9 @@ func (t Tags) MbzReleaseTrackID() string {
return t.getMbzID("musicbrainz_releasetrackid", "musicbrainz release track id")
}
func (t Tags) MbzTrackID() string { return t.getMbzID("musicbrainz_trackid", "musicbrainz track id") }
func (t Tags) MbzRecordingID() string {
return t.getMbzID("musicbrainz_trackid", "musicbrainz track id")
}
func (t Tags) MbzAlbumID() string { return t.getMbzID("musicbrainz_albumid", "musicbrainz album id") }
func (t Tags) MbzArtistID() string {
return t.getMbzID("musicbrainz_artistid", "musicbrainz artist id")
@@ -144,6 +149,12 @@ func (t Tags) ModificationTime() time.Time { return t.fileInfo.ModTime() }
func (t Tags) Size() int64 { return t.fileInfo.Size() }
func (t Tags) FilePath() string { return t.filePath }
func (t Tags) Suffix() string { return strings.ToLower(strings.TrimPrefix(path.Ext(t.filePath), ".")) }
func (t Tags) BirthTime() time.Time {
if ts := times.Get(t.fileInfo); ts.HasBirthTime() {
return ts.BirthTime()
}
return time.Now()
}
// Replaygain Properties
func (t Tags) RGAlbumGain() float64 { return t.getGainValue("replaygain_album_gain") }

View File

@@ -36,7 +36,7 @@ var _ = Describe("Tags", func() {
"musicbrainz_artistid": {"89ad4ac3-39f7-470e-963a-56509c546377"},
"musicbrainz_albumartistid": {"ada7a83c-e3e1-40f1-93f9-3e73dbc9298a"},
}
Expect(md.MbzTrackID()).To(Equal("8f84da07-09a0-477b-b216-cc982dabcde1"))
Expect(md.MbzRecordingID()).To(Equal("8f84da07-09a0-477b-b216-cc982dabcde1"))
Expect(md.MbzReleaseTrackID()).To(Equal("6caf16d3-0b20-3fe6-8020-52e31831bc11"))
Expect(md.MbzAlbumID()).To(Equal("f68c985d-f18b-4f4a-b7f0-87837cf3fbf9"))
Expect(md.MbzArtistID()).To(Equal("89ad4ac3-39f7-470e-963a-56509c546377"))
@@ -50,7 +50,7 @@ var _ = Describe("Tags", func() {
"musicbrainz_artistid": {"200455"},
"musicbrainz_albumartistid": {"194"},
}
Expect(md.MbzTrackID()).To(Equal(""))
Expect(md.MbzRecordingID()).To(Equal(""))
Expect(md.MbzAlbumID()).To(Equal(""))
Expect(md.MbzArtistID()).To(Equal(""))
Expect(md.MbzAlbumArtistID()).To(Equal(""))

View File

@@ -56,15 +56,15 @@ var _ = Describe("Tags", func() {
m = mds["tests/fixtures/test.ogg"]
Expect(err).To(BeNil())
Expect(m.Title()).To(BeEmpty())
Expect(m.Title()).To(Equal("Title"))
Expect(m.HasPicture()).To(BeFalse())
Expect(m.Duration()).To(BeNumerically("~", 1.04, 0.01))
Expect(m.Suffix()).To(Equal("ogg"))
Expect(m.FilePath()).To(Equal("tests/fixtures/test.ogg"))
Expect(m.Size()).To(Equal(int64(5178)))
Expect(m.Size()).To(Equal(int64(6333)))
// TabLib 1.12 returns 18, previous versions return 39.
// See https://github.com/taglib/taglib/commit/2f238921824741b2cfe6fbfbfc9701d9827ab06b
Expect(m.BitRate()).To(BeElementOf(18, 39, 40))
Expect(m.BitRate()).To(BeElementOf(18, 39, 40, 49))
})
})
})

View File

@@ -52,6 +52,10 @@ var _ = Describe("Extractor", func() {
Expect(m).To(HaveKeyWithValue("comment", []string{"Comment1\nComment2"}))
Expect(m).To(HaveKeyWithValue("lyrics", []string{"Lyrics 1\rLyrics 2"}))
Expect(m).To(HaveKeyWithValue("bpm", []string{"123"}))
Expect(m).To(HaveKeyWithValue("replaygain_album_gain", []string{"+3.21518 dB"}))
Expect(m).To(HaveKeyWithValue("replaygain_album_peak", []string{"0.9125"}))
Expect(m).To(HaveKeyWithValue("replaygain_track_gain", []string{"-1.48 dB"}))
Expect(m).To(HaveKeyWithValue("replaygain_track_peak", []string{"0.4512"}))
Expect(m).To(HaveKeyWithValue("tracknumber", []string{"2/10"}))
m = m.Map(e.CustomMappings())
@@ -59,7 +63,6 @@ var _ = Describe("Extractor", func() {
m = mds["tests/fixtures/test.ogg"]
Expect(err).To(BeNil())
Expect(m).ToNot(HaveKey("title"))
Expect(m).ToNot(HaveKey("has_picture"))
Expect(m).To(HaveKeyWithValue("duration", []string{"1.04"}))
Expect(m).To(HaveKeyWithValue("fbpm", []string{"141.7"}))
@@ -67,11 +70,11 @@ var _ = Describe("Extractor", func() {
// TabLib 1.12 returns 18, previous versions return 39.
// See https://github.com/taglib/taglib/commit/2f238921824741b2cfe6fbfbfc9701d9827ab06b
Expect(m).To(HaveKey("bitrate"))
Expect(m["bitrate"][0]).To(BeElementOf("18", "39", "40"))
Expect(m["bitrate"][0]).To(BeElementOf("18", "39", "40", "49"))
})
DescribeTable("ReplayGain",
func(file, albumGain, albumPeak, trackGain, trackPeak string) {
DescribeTable("Format-Specific tests",
func(file, duration, channels, albumGain, albumPeak, trackGain, trackPeak string) {
file = "tests/fixtures/" + file
mds, err := e.Parse(file)
Expect(err).NotTo(HaveOccurred())
@@ -83,10 +86,54 @@ var _ = Describe("Extractor", func() {
Expect(m).To(HaveKeyWithValue("replaygain_album_peak", []string{albumPeak}))
Expect(m).To(HaveKeyWithValue("replaygain_track_gain", []string{trackGain}))
Expect(m).To(HaveKeyWithValue("replaygain_track_peak", []string{trackPeak}))
Expect(m).To(HaveKeyWithValue("title", []string{"Title", "Title"}))
Expect(m).To(HaveKeyWithValue("album", []string{"Album", "Album"}))
Expect(m).To(HaveKeyWithValue("artist", []string{"Artist", "Artist"}))
Expect(m).To(HaveKeyWithValue("albumartist", []string{"Album Artist"}))
Expect(m).To(HaveKeyWithValue("compilation", []string{"1"}))
Expect(m).To(HaveKeyWithValue("genre", []string{"Rock"}))
Expect(m).To(HaveKeyWithValue("date", []string{"2014", "2014"}))
Expect(m).To(HaveKey("discnumber"))
discno := m["discnumber"]
Expect(discno).To(HaveLen(1))
Expect(discno[0]).To(BeElementOf([]string{"1", "1/2"}))
Expect(m).NotTo(HaveKeyWithValue("has_picture", []string{"true"}))
Expect(m).To(HaveKeyWithValue("duration", []string{duration}))
Expect(m).To(HaveKeyWithValue("channels", []string{channels}))
Expect(m).To(HaveKeyWithValue("comment", []string{"Comment1\nComment2"}))
Expect(m).To(HaveKeyWithValue("lyrics", []string{"Lyrics1\nLyrics 2"}))
Expect(m).To(HaveKeyWithValue("bpm", []string{"123"}))
Expect(m).To(HaveKey("tracknumber"))
trackNo := m["tracknumber"]
Expect(trackNo).To(HaveLen(1))
Expect(trackNo[0]).To(BeElementOf([]string{"3", "3/10"}))
},
Entry("Correctly parses m4a (aac) gain tags", "01 Invisible (RED) Edit Version.m4a", "0.37", "0.48", "0.37", "0.48"),
Entry("correctly parses mp3 tags", "test.mp3", "+3.21518 dB", "0.9125", "-1.48 dB", "0.4512"),
Entry("correctly parses ogg (vorbis) tags", "test.ogg", "+7.64 dB", "0.11772506", "+7.64 dB", "0.11772506"),
// ffmpeg -f lavfi -i "sine=frequency=1200:duration=1" test.flac
Entry("correctly parses flac tags", "test.flac", "1.00", "1", "+4.06 dB", "0.12496948", "+4.06 dB", "0.12496948"),
Entry("Correctly parses m4a (aac) gain tags", "01 Invisible (RED) Edit Version.m4a", "1.04", "2", "0.37", "0.48", "0.37", "0.48"),
Entry("correctly parses ogg (vorbis) tags", "test.ogg", "1.04", "2", "+7.64 dB", "0.11772506", "+7.64 dB", "0.11772506"),
// ffmpeg -f lavfi -i "sine=frequency=900:duration=1" test.wma
Entry("correctly parses wma/asf tags", "test.wma", "1.02", "1", "3.27 dB", "0.132914", "3.27 dB", "0.132914"),
// ffmpeg -f lavfi -i "sine=frequency=800:duration=1" test.wv
Entry("correctly parses wv (wavpak) tags", "test.wv", "1.00", "1", "3.43 dB", "0.125061", "3.43 dB", "0.125061"),
// TODO - these breaks in the pipeline as it uses TabLib 1.11. Once Ubuntu 24.04 is released we can uncomment these tests
// ffmpeg -f lavfi -i "sine=frequency=1000:duration=1" test.wav
//Entry("correctly parses wav tags", "test.wav", "1.00", "1", "3.06 dB", "0.125056", "3.06 dB", "0.125056"),
// ffmpeg -f lavfi -i "sine=frequency=1400:duration=1" test.aiff
//Entry("correctly parses aiff tags", "test.aiff", "1.00", "1", "2.00 dB", "0.124972", "2.00 dB", "0.124972"),
)
})

View File

@@ -100,6 +100,29 @@ int taglib_read(const FILENAME_CHAR_T *filename, unsigned long id) {
}
}
// WMA/ASF files may have additional tags not captured by the general iterator
TagLib::ASF::File *asfFile(dynamic_cast<TagLib::ASF::File *>(f.file()));
if (asfFile != NULL)
{
const TagLib::ASF::Tag *asfTags{asfFile->tag()};
const auto itemListMap = asfTags->attributeListMap();
for (const auto item : itemListMap) {
char *key = ::strdup(item.first.toCString(true));
char *val = ::strdup(item.second.front().toString().toCString());
go_map_put_str(id, key, val);
free(key);
free(val);
}
// Compilation tag needs to be handled differently
const auto compilation = asfTags->attribute("WM/IsCompilation");
if (!compilation.isEmpty()) {
char *val = ::strdup(compilation.front().toString().toCString());
go_map_put_str(id, (char *)"compilation", val);
free(val);
}
}
if (has_cover(f)) {
go_map_put_str(id, (char *)"has_picture", (char *)"true");
}

View File

@@ -36,6 +36,9 @@ func (s *playlistImporter) processPlaylists(ctx context.Context, dir string) int
return count
}
for _, f := range files {
if strings.HasPrefix(f.Name(), ".") {
continue
}
if !model.IsValidPlaylist(f.Name()) {
continue
}

View File

@@ -59,7 +59,7 @@ var _ = Describe("playlistImporter", func() {
conf.Server.PlaylistsPath = "."
ps = newPlaylistImporter(ds, pls, cw, "tests/fixtures/playlists")
Expect(ps.processPlaylists(ctx, "tests/fixtures/playlists")).To(Equal(int64(3)))
Expect(ps.processPlaylists(ctx, "tests/fixtures/playlists")).To(Equal(int64(5)))
Expect(ps.processPlaylists(ctx, "tests/fixtures/playlists/subfolder1")).To(Equal(int64(0)))
})

View File

@@ -142,7 +142,8 @@ func (r *refresher) refreshArtists(ctx context.Context, ids ...string) error {
// Force a external metadata lookup on next access
a.ExternalInfoUpdatedAt = time.Time{}
err := repo.Put(&a)
// Do not remove old metadata
err := repo.Put(&a, "album_count", "genres", "external_info_updated_at", "mbz_artist_id", "name", "order_artist_name", "size", "sort_artist_name", "song_count")
if err != nil {
return err
}

View File

@@ -10,9 +10,14 @@ var _ = Describe("TagScanner", func() {
It("return all audio files from the folder", func() {
files, err := loadAllAudioFiles("tests/fixtures")
Expect(err).ToNot(HaveOccurred())
Expect(files).To(HaveLen(5))
Expect(files).To(HaveKey("tests/fixtures/test.ogg"))
Expect(files).To(HaveLen(10))
Expect(files).To(HaveKey("tests/fixtures/test.aiff"))
Expect(files).To(HaveKey("tests/fixtures/test.flac"))
Expect(files).To(HaveKey("tests/fixtures/test.mp3"))
Expect(files).To(HaveKey("tests/fixtures/test.ogg"))
Expect(files).To(HaveKey("tests/fixtures/test.wav"))
Expect(files).To(HaveKey("tests/fixtures/test.wma"))
Expect(files).To(HaveKey("tests/fixtures/test.wv"))
Expect(files).To(HaveKey("tests/fixtures/test_no_read_permission.ogg"))
Expect(files).To(HaveKey("tests/fixtures/01 Invisible (RED) Edit Version.mp3"))
Expect(files).To(HaveKey("tests/fixtures/01 Invisible (RED) Edit Version.m4a"))

View File

@@ -35,7 +35,7 @@ var _ = Describe("walk_dir_tree", func() {
Expect(collected[baseDir]).To(MatchFields(IgnoreExtras, Fields{
"Images": BeEmpty(),
"HasPlaylist": BeFalse(),
"AudioFilesCount": BeNumerically("==", 6),
"AudioFilesCount": BeNumerically("==", 11),
}))
Expect(collected[filepath.Join(baseDir, "artist", "an-album")]).To(MatchFields(IgnoreExtras, Fields{
"Images": ConsistOf("cover.jpg", "front.png", "artist.png"),

View File

@@ -14,12 +14,13 @@ import (
type Router struct {
http.Handler
ds model.DataStore
share core.Share
ds model.DataStore
share core.Share
playlists core.Playlists
}
func New(ds model.DataStore, share core.Share) *Router {
r := &Router{ds: ds, share: share}
func New(ds model.DataStore, share core.Share, playlists core.Playlists) *Router {
r := &Router{ds: ds, share: share, playlists: playlists}
r.Handler = r.routes()
return r
}
@@ -40,13 +41,13 @@ func (n *Router) routes() http.Handler {
n.R(r, "/artist", model.Artist{}, false)
n.R(r, "/genre", model.Genre{}, false)
n.R(r, "/player", model.Player{}, true)
n.R(r, "/playlist", model.Playlist{}, true)
n.R(r, "/transcoding", model.Transcoding{}, conf.Server.EnableTranscodingConfig)
n.R(r, "/radio", model.Radio{}, true)
if conf.Server.EnableSharing {
n.RX(r, "/share", n.share.NewRepository, true)
}
n.addPlaylistRoute(r)
n.addPlaylistTrackRoute(r)
// Keepalive endpoint to be used to keep the session valid (ex: while playing songs)
@@ -82,6 +83,30 @@ func (n *Router) RX(r chi.Router, pathPrefix string, constructor rest.Repository
})
}
func (n *Router) addPlaylistRoute(r chi.Router) {
constructor := func(ctx context.Context) rest.Repository {
return n.ds.Resource(ctx, model.Playlist{})
}
r.Route("/playlist", func(r chi.Router) {
r.Get("/", rest.GetAll(constructor))
r.Post("/", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Content-type") == "application/json" {
rest.Post(constructor)(w, r)
return
}
createPlaylistFromM3U(n.playlists)(w, r)
})
r.Route("/{id}", func(r chi.Router) {
r.Use(server.URLParamsMiddleware)
r.Get("/", rest.Get(constructor))
r.Put("/", rest.Put(constructor))
r.Delete("/", rest.Delete(constructor))
})
})
}
func (n *Router) addPlaylistTrackRoute(r chi.Router) {
r.Route("/playlist/{playlistId}/tracks", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {

View File

@@ -11,6 +11,7 @@ import (
"github.com/deluan/rest"
"github.com/go-chi/chi/v5"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils"
@@ -42,6 +43,26 @@ func getPlaylist(ds model.DataStore) http.HandlerFunc {
}
}
func createPlaylistFromM3U(playlists core.Playlists) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
pls, err := playlists.ImportM3U(ctx, r.Body)
if err != nil {
log.Error(r.Context(), "Error parsing playlist", err)
// TODO: consider returning StatusBadRequest for playlists that are malformed
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
_, err = w.Write([]byte(pls.ToM3U8()))
if err != nil {
log.Error(ctx, "Error sending m3u contents", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
func handleExportPlaylist(ds model.DataStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

View File

@@ -20,7 +20,7 @@ var _ = Describe("AbsoluteURL", func() {
Expect(actual).To(Equal("https://myserver.com/share/img/123?a=xyz"))
})
It("does not override provided schema/host", func() {
r, _ := http.NewRequest("GET", "http://127.0.0.1/rest/ping?id=123", nil)
r, _ := http.NewRequest("GET", "http://localhost/rest/ping?id=123", nil)
actual := AbsoluteURL(r, "http://public.myserver.com/share/img/123", url.Values{"a": []string{"xyz"}})
Expect(actual).To(Equal("http://public.myserver.com/share/img/123?a=xyz"))
})
@@ -35,7 +35,7 @@ var _ = Describe("AbsoluteURL", func() {
Expect(actual).To(Equal("https://myserver.com/music/share/img/123?a=xyz"))
})
It("does not override provided schema/host", func() {
r, _ := http.NewRequest("GET", "http://127.0.0.1/rest/ping?id=123", nil)
r, _ := http.NewRequest("GET", "http://localhost/rest/ping?id=123", nil)
actual := AbsoluteURL(r, "http://public.myserver.com/share/img/123", url.Values{"a": []string{"xyz"}})
Expect(actual).To(Equal("http://public.myserver.com/share/img/123?a=xyz"))
})
@@ -52,7 +52,7 @@ var _ = Describe("AbsoluteURL", func() {
Expect(actual).To(Equal("https://myserver.com:8080/music/share/img/123?a=xyz"))
})
It("does not override provided schema/host", func() {
r, _ := http.NewRequest("GET", "http://127.0.0.1/rest/ping?id=123", nil)
r, _ := http.NewRequest("GET", "http://localhost/rest/ping?id=123", nil)
actual := AbsoluteURL(r, "http://public.myserver.com/share/img/123", url.Values{"a": []string{"xyz"}})
Expect(actual).To(Equal("http://public.myserver.com/share/img/123?a=xyz"))
})

View File

@@ -179,8 +179,15 @@ func (api *Router) routes() http.Handler {
h(r, "getOpenSubsonicExtensions", api.GetOpenSubsonicExtensions)
})
if conf.Server.Jukebox.Enabled {
r.Group(func(r chi.Router) {
h(r, "jukeboxControl", api.JukeboxControl)
})
} else {
h501(r, "jukeboxControl")
}
// Not Implemented (yet?)
h501(r, "jukeboxControl")
h501(r, "getPodcasts", "getNewestPodcasts", "refreshPodcasts", "createPodcastChannel", "deletePodcastChannel",
"deletePodcastEpisode", "downloadPodcastEpisode")
h501(r, "createUser", "updateUser", "deleteUser", "changePassword")

142
server/subsonic/jukebox.go Normal file
View File

@@ -0,0 +1,142 @@
package subsonic
import (
"fmt"
"net/http"
"strconv"
"github.com/navidrome/navidrome/core/playback"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/server/subsonic/responses"
)
const (
ActionGet = "get"
ActionStatus = "status"
ActionSet = "set"
ActionStart = "start"
ActionStop = "stop"
ActionSkip = "skip"
ActionAdd = "add"
ActionClear = "clear"
ActionRemove = "remove"
ActionShuffle = "shuffle"
ActionSetGain = "setGain"
)
func (api *Router) JukeboxControl(r *http.Request) (*responses.Subsonic, error) {
ctx := r.Context()
user := getUser(ctx)
actionString, err := requiredParamString(r, "action")
if err != nil {
return nil, err
}
pbServer := playback.GetInstance()
pb, err := pbServer.GetDeviceForUser(user.UserName)
if err != nil {
return nil, err
}
log.Debug(fmt.Sprintf("processing action: %s", actionString))
switch actionString {
case ActionGet:
mediafiles, status, err := pb.Get(ctx)
if err != nil {
return nil, err
}
playlist := responses.JukeboxPlaylist{
JukeboxStatus: *deviceStatusToJukeboxStatus(status),
Entry: childrenFromMediaFiles(ctx, mediafiles),
}
response := newResponse()
response.JukeboxPlaylist = &playlist
return response, nil
case ActionStatus:
return createResponse(pb.Status(ctx))
case ActionSet:
ids, err := requiredParamStrings(r, "id")
if err != nil {
return nil, newError(responses.ErrorMissingParameter, "missing parameter id, err: %s", err)
}
status, err := pb.Set(ctx, ids)
if err != nil {
return nil, err
}
return statusResponse(status), nil
case ActionStart:
return createResponse(pb.Start(ctx))
case ActionStop:
return createResponse(pb.Stop(ctx))
case ActionSkip:
index, err := requiredParamInt(r, "index")
if err != nil {
return nil, newError(responses.ErrorMissingParameter, "missing parameter index, err: %s", err)
}
offset, err := requiredParamInt(r, "offset")
if err != nil {
offset = 0
}
return createResponse(pb.Skip(ctx, index, offset))
case ActionAdd:
ids, err := requiredParamStrings(r, "id")
if err != nil {
return nil, newError(responses.ErrorMissingParameter, "missing parameter id, err: %s", err)
}
return createResponse(pb.Add(ctx, ids))
case ActionClear:
return createResponse(pb.Clear(ctx))
case ActionRemove:
index, err := requiredParamInt(r, "index")
if err != nil {
return nil, err
}
return createResponse(pb.Remove(ctx, index))
case ActionShuffle:
return createResponse(pb.Shuffle(ctx))
case ActionSetGain:
gainStr, err := requiredParamString(r, "gain")
if err != nil {
return nil, newError(responses.ErrorMissingParameter, "missing parameter gain, err: %s", err)
}
gain, err := strconv.ParseFloat(gainStr, 32)
if err != nil {
return nil, newError(responses.ErrorMissingParameter, "error parsing gain integer value, err: %s", err)
}
return createResponse(pb.SetGain(ctx, float32(gain)))
default:
return nil, newError(responses.ErrorMissingParameter, "Unknown action: %s", actionString)
}
}
// createResponse is to shorten the case-switch in the JukeboxController
func createResponse(status playback.DeviceStatus, err error) (*responses.Subsonic, error) {
if err != nil {
return nil, err
}
return statusResponse(status), nil
}
func statusResponse(status playback.DeviceStatus) *responses.Subsonic {
response := newResponse()
response.JukeboxStatus = deviceStatusToJukeboxStatus(status)
return response
}
func deviceStatusToJukeboxStatus(status playback.DeviceStatus) *responses.JukeboxStatus {
return &responses.JukeboxStatus{
CurrentIndex: int32(status.CurrentIndex),
Playing: status.Playing,
Gain: status.Gain,
Position: int32(status.Position),
}
}

View File

@@ -51,7 +51,11 @@ type Subsonic struct {
ScanStatus *ScanStatus `xml:"scanStatus,omitempty" json:"scanStatus,omitempty"`
Lyrics *Lyrics `xml:"lyrics,omitempty" json:"lyrics,omitempty"`
InternetRadioStations *InternetRadioStations `xml:"internetRadioStations,omitempty" json:"internetRadioStations,omitempty"`
InternetRadioStations *InternetRadioStations `xml:"internetRadioStations,omitempty" json:"internetRadioStations,omitempty"`
JukeboxStatus *JukeboxStatus `xml:"jukeboxStatus,omitempty" json:"jukeboxStatus,omitempty"`
JukeboxPlaylist *JukeboxPlaylist `xml:"jukeboxPlaylist,omitempty" json:"jukeboxPlaylist,omitempty"`
OpenSubsonicExtensions *OpenSubsonicExtensions `xml:"openSubsonicExtensions,omitempty" json:"openSubsonicExtensions,omitempty"`
}
@@ -402,4 +406,15 @@ type Radio struct {
HomepageUrl string `xml:"homePageUrl,omitempty,attr" json:"homePageUrl,omitempty"`
}
type JukeboxStatus struct {
CurrentIndex int32 `xml:"currentIndex,attr" json:"currentIndex"`
Playing bool `xml:"playing,attr" json:"playing"`
Gain float32 `xml:"gain,attr" json:"gain"`
Position int32 `xml:"position,omitempty,attr" json:"position"`
}
type JukeboxPlaylist struct {
JukeboxStatus
Entry []Child `xml:"entry,omitempty" json:"entry,omitempty"`
}
type OpenSubsonicExtensions struct{}

View File

@@ -625,7 +625,8 @@ var _ = Describe("Responses", func() {
Context("with data", func() {
BeforeEach(func() {
t, _ := time.Parse(time.RFC822, time.RFC822)
timeFmt := "2006-01-02 15:04:00"
t, _ := time.Parse(timeFmt, timeFmt)
response.ScanStatus = &ScanStatus{
Scanning: true,
FolderCount: 123,

View File

@@ -23,6 +23,7 @@ func (api *Router) GetUser(r *http.Request) (*responses.Subsonic, error) {
response.User.ScrobblingEnabled = true
response.User.DownloadRole = conf.Server.EnableDownloads
response.User.ShareRole = conf.Server.EnableSharing
response.User.JukeboxRole = conf.Server.Jukebox.Enabled
return response, nil
}
@@ -39,6 +40,7 @@ func (api *Router) GetUsers(r *http.Request) (*responses.Subsonic, error) {
user.ScrobblingEnabled = true
user.DownloadRole = conf.Server.EnableDownloads
user.ShareRole = conf.Server.EnableSharing
user.JukeboxRole = conf.Server.Jukebox.Enabled
response := newResponse()
response.Users = &responses.Users{User: []responses.User{user}}
return response, nil

View File

Binary file not shown.

View File

@@ -1 +1 @@
{"listen_type": "playing_now", "payload": [{"track_metadata": { "artist_name": "Track Artist", "track_name": "Track Title", "release_name": "Track Album", "additional_info": { "tracknumber": 1, "track_mbid": "mbz-123", "artist_mbids": ["mbz-789"], "release_mbid": "mbz-456"}}}]}
{"listen_type": "playing_now", "payload": [{"track_metadata": { "artist_name": "Track Artist", "track_name": "Track Title", "release_name": "Track Album", "additional_info": { "tracknumber": 1, "recording_mbid": "mbz-123", "artist_mbids": ["mbz-789"], "release_mbid": "mbz-456", "duration_ms": 142200}}}]}

View File

@@ -1 +1 @@
{"listen_type": "single", "payload": [{"listened_at": 1635000000, "track_metadata": { "artist_name": "Track Artist", "track_name": "Track Title", "release_name": "Track Album", "additional_info": { "tracknumber": 1, "track_mbid": "mbz-123", "artist_mbids": ["mbz-789"], "release_mbid": "mbz-456"}}}]}
{"listen_type": "single", "payload": [{"listened_at": 1635000000, "track_metadata": { "artist_name": "Track Artist", "track_name": "Track Title", "release_name": "Track Album", "additional_info": { "tracknumber": 1, "recording_mbid": "mbz-123", "artist_mbids": ["mbz-789"], "release_mbid": "mbz-456", "duration_ms": 142200}}}]}

View File

@@ -0,0 +1,4 @@
#PLAYLIST:playlist 1
tests/fixtures/test.mp3
tests/fixtures/test.ogg
file:///tests/fixtures/01%20Invisible%20(RED)%20Edit%20Version.mp3

3
tests/fixtures/playlists/pls-post.m3u vendored Normal file
View File

@@ -0,0 +1,3 @@
tests/fixtures/test.mp3
tests/fixtures/test.ogg
file:///tests/fixtures/01%20Invisible%20(RED)%20Edit%20Version.mp3

View File

@@ -0,0 +1,2 @@
test.mp3
test.ogg

View File

@@ -0,0 +1,2 @@
test.mp3
test.ogg

View File

@@ -1 +0,0 @@
INVALID

BIN
tests/fixtures/test.aiff vendored Normal file
View File

Binary file not shown.

BIN
tests/fixtures/test.flac vendored Normal file
View File

Binary file not shown.

View File

Binary file not shown.

BIN
tests/fixtures/test.wav vendored Normal file
View File

Binary file not shown.

BIN
tests/fixtures/test.wma vendored Normal file
View File

Binary file not shown.

BIN
tests/fixtures/test.wv vendored Normal file
View File

Binary file not shown.

View File

@@ -50,7 +50,7 @@ func (m *MockArtistRepo) Get(id string) (*model.Artist, error) {
return nil, model.ErrNotFound
}
func (m *MockArtistRepo) Put(ar *model.Artist) error {
func (m *MockArtistRepo) Put(ar *model.Artist, columsToUpdate ...string) error {
if m.err {
return errors.New("error")
}

View File

@@ -33,6 +33,20 @@ func (ff *MockFFmpeg) ExtractImage(context.Context, string) (io.ReadCloser, erro
return ff, nil
}
func (ff *MockFFmpeg) ConvertToFLAC(context.Context, string) (io.ReadCloser, error) {
if ff.Error != nil {
return nil, ff.Error
}
return ff, nil
}
func (ff *MockFFmpeg) ConvertToWAV(context.Context, string) (io.ReadCloser, error) {
if ff.Error != nil {
return nil, ff.Error
}
return ff, nil
}
func (ff *MockFFmpeg) Probe(context.Context, []string) (string, error) {
if ff.Error != nil {
return "", ff.Error

452
ui/package-lock.json generated
View File

@@ -37,7 +37,7 @@
"react-image-lightbox": "^5.1.4",
"react-measure": "^2.5.2",
"react-redux": "^7.2.9",
"react-router-dom": "^5.3.0",
"react-router-dom": "^5.3.4",
"redux": "^4.2.0",
"redux-saga": "^1.1.3",
"uuid": "^8.3.2"
@@ -74,17 +74,80 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
"integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
"version": "7.22.13",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
"integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
"dev": true,
"dependencies": {
"@babel/highlight": "^7.18.6"
"@babel/highlight": "^7.22.13",
"chalk": "^2.4.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/code-frame/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/code-frame/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/code-frame/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/@babel/code-frame/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
"node_modules/@babel/code-frame/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/code-frame/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/compat-data": {
"version": "7.19.3",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.3.tgz",
@@ -170,13 +233,14 @@
}
},
"node_modules/@babel/generator": {
"version": "7.19.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.3.tgz",
"integrity": "sha512-fqVZnmp1ncvZU757UzDheKZpfPgatqY59XtW2/j/18H7u76akb8xqvjw82f+i2UKd/ksYsSick/BCLQUUtJ/qQ==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
"integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
"dev": true,
"dependencies": {
"@babel/types": "^7.19.3",
"@babel/types": "^7.23.0",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
},
"engines": {
@@ -313,9 +377,9 @@
}
},
"node_modules/@babel/helper-environment-visitor": {
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
"integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
"integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -334,25 +398,25 @@
}
},
"node_modules/@babel/helper-function-name": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz",
"integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
"integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dev": true,
"dependencies": {
"@babel/template": "^7.18.10",
"@babel/types": "^7.19.0"
"@babel/template": "^7.22.15",
"@babel/types": "^7.23.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-hoist-variables": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
"integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
"integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
"dev": true,
"dependencies": {
"@babel/types": "^7.18.6"
"@babel/types": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
@@ -481,30 +545,30 @@
}
},
"node_modules/@babel/helper-split-export-declaration": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
"integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
"integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
"dev": true,
"dependencies": {
"@babel/types": "^7.18.6"
"@babel/types": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz",
"integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==",
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.19.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -549,13 +613,13 @@
}
},
"node_modules/@babel/highlight": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
"integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.18.6",
"chalk": "^2.0.0",
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0"
},
"engines": {
@@ -625,9 +689,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.19.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.3.tgz",
"integrity": "sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
"integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@@ -2003,33 +2067,33 @@
}
},
"node_modules/@babel/template": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
"integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==",
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/parser": "^7.18.10",
"@babel/types": "^7.18.10"
"@babel/code-frame": "^7.22.13",
"@babel/parser": "^7.22.15",
"@babel/types": "^7.22.15"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.19.3",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.3.tgz",
"integrity": "sha512-qh5yf6149zhq2sgIXmwjnsvmnNQC2iw70UFjp4olxucKrWd/dvlUsBI88VSLUsnMNF7/vnOiA+nk1+yLoCqROQ==",
"version": "7.23.2",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
"integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.19.3",
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-function-name": "^7.19.0",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.19.3",
"@babel/types": "^7.19.3",
"@babel/code-frame": "^7.22.13",
"@babel/generator": "^7.23.0",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.23.0",
"@babel/types": "^7.23.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -2038,13 +2102,13 @@
}
},
"node_modules/@babel/types": {
"version": "7.19.3",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.3.tgz",
"integrity": "sha512-hGCaQzIY22DJlDh9CH7NOxgKkFjBk0Cw9xDO1Xmh2151ti7wiGfQ3LauXzL4HP1fmFlTX6XjpRETTpUcv7wQLw==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.18.10",
"@babel/helper-validator-identifier": "^7.19.1",
"@babel/helper-string-parser": "^7.22.5",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
@@ -3187,13 +3251,13 @@
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.15",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz",
"integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==",
"version": "0.3.20",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@leichtgewicht/ip-codec": {
@@ -13773,15 +13837,6 @@
"node": ">=4"
}
},
"node_modules/mini-create-react-context": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
"dependencies": {
"@babel/runtime": "^7.12.1",
"tiny-warning": "^1.0.3"
}
},
"node_modules/mini-css-extract-plugin": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.1.tgz",
@@ -16994,15 +17049,14 @@
}
},
"node_modules/react-router": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
"integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
@@ -17014,15 +17068,15 @@
}
},
"node_modules/react-router-dom": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz",
"integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==",
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
"integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.1",
"react-router": "5.3.4",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
@@ -19215,9 +19269,9 @@
}
},
"node_modules/tough-cookie": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
"integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
"integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
"dev": true,
"dependencies": {
"psl": "^1.1.33",
@@ -20217,9 +20271,9 @@
}
},
"node_modules/word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@@ -20809,12 +20863,65 @@
}
},
"@babel/code-frame": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
"integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
"version": "7.22.13",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
"integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
"dev": true,
"requires": {
"@babel/highlight": "^7.18.6"
"@babel/highlight": "^7.22.13",
"chalk": "^2.4.2"
},
"dependencies": {
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"@babel/compat-data": {
@@ -20880,13 +20987,14 @@
}
},
"@babel/generator": {
"version": "7.19.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.3.tgz",
"integrity": "sha512-fqVZnmp1ncvZU757UzDheKZpfPgatqY59XtW2/j/18H7u76akb8xqvjw82f+i2UKd/ksYsSick/BCLQUUtJ/qQ==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
"integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
"dev": true,
"requires": {
"@babel/types": "^7.19.3",
"@babel/types": "^7.23.0",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
},
"dependencies": {
@@ -20990,9 +21098,9 @@
}
},
"@babel/helper-environment-visitor": {
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
"integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
"integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"dev": true
},
"@babel/helper-explode-assignable-expression": {
@@ -21005,22 +21113,22 @@
}
},
"@babel/helper-function-name": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz",
"integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
"integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dev": true,
"requires": {
"@babel/template": "^7.18.10",
"@babel/types": "^7.19.0"
"@babel/template": "^7.22.15",
"@babel/types": "^7.23.0"
}
},
"@babel/helper-hoist-variables": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
"integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
"integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
"dev": true,
"requires": {
"@babel/types": "^7.18.6"
"@babel/types": "^7.22.5"
}
},
"@babel/helper-member-expression-to-functions": {
@@ -21116,24 +21224,24 @@
}
},
"@babel/helper-split-export-declaration": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
"integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
"integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
"dev": true,
"requires": {
"@babel/types": "^7.18.6"
"@babel/types": "^7.22.5"
}
},
"@babel/helper-string-parser": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz",
"integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==",
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
"dev": true
},
"@babel/helper-validator-identifier": {
"version": "7.19.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true
},
"@babel/helper-validator-option": {
@@ -21166,13 +21274,13 @@
}
},
"@babel/highlight": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
"integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.18.6",
"chalk": "^2.0.0",
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0"
},
"dependencies": {
@@ -21229,9 +21337,9 @@
}
},
"@babel/parser": {
"version": "7.19.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.3.tgz",
"integrity": "sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
"integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
"dev": true
},
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
@@ -22173,42 +22281,42 @@
}
},
"@babel/template": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
"integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==",
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.18.6",
"@babel/parser": "^7.18.10",
"@babel/types": "^7.18.10"
"@babel/code-frame": "^7.22.13",
"@babel/parser": "^7.22.15",
"@babel/types": "^7.22.15"
}
},
"@babel/traverse": {
"version": "7.19.3",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.3.tgz",
"integrity": "sha512-qh5yf6149zhq2sgIXmwjnsvmnNQC2iw70UFjp4olxucKrWd/dvlUsBI88VSLUsnMNF7/vnOiA+nk1+yLoCqROQ==",
"version": "7.23.2",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
"integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.19.3",
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-function-name": "^7.19.0",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.19.3",
"@babel/types": "^7.19.3",
"@babel/code-frame": "^7.22.13",
"@babel/generator": "^7.23.0",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.23.0",
"@babel/types": "^7.23.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
}
},
"@babel/types": {
"version": "7.19.3",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.3.tgz",
"integrity": "sha512-hGCaQzIY22DJlDh9CH7NOxgKkFjBk0Cw9xDO1Xmh2151ti7wiGfQ3LauXzL4HP1fmFlTX6XjpRETTpUcv7wQLw==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"dev": true,
"requires": {
"@babel/helper-string-parser": "^7.18.10",
"@babel/helper-validator-identifier": "^7.19.1",
"@babel/helper-string-parser": "^7.22.5",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
}
},
@@ -23034,13 +23142,13 @@
"dev": true
},
"@jridgewell/trace-mapping": {
"version": "0.3.15",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz",
"integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==",
"version": "0.3.20",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
"dev": true,
"requires": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"@leichtgewicht/ip-codec": {
@@ -31264,15 +31372,6 @@
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true
},
"mini-create-react-context": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
"requires": {
"@babel/runtime": "^7.12.1",
"tiny-warning": "^1.0.3"
}
},
"mini-css-extract-plugin": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.1.tgz",
@@ -33518,15 +33617,14 @@
"dev": true
},
"react-router": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
"integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
"requires": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
@@ -33535,15 +33633,15 @@
}
},
"react-router-dom": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz",
"integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==",
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
"integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
"requires": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.1",
"react-router": "5.3.4",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}
@@ -35189,9 +35287,9 @@
"dev": true
},
"tough-cookie": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
"integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
"integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
"dev": true,
"requires": {
"psl": "^1.1.33",
@@ -35945,9 +36043,9 @@
}
},
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true
},
"workbox-background-sync": {

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