mirror of
https://github.com/navidrome/navidrome.git
synced 2025-12-31 19:08:06 -05:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cf98af9da | ||
|
|
40f7170930 | ||
|
|
4a8f176f0d | ||
|
|
4507895d58 | ||
|
|
90c2d7d1cf | ||
|
|
f62231f728 | ||
|
|
fd1e06049f | ||
|
|
7dfe29af5e | ||
|
|
8e03d7d013 | ||
|
|
960415ed95 | ||
|
|
ea231fe265 | ||
|
|
09e52eba87 | ||
|
|
2650e4c27c | ||
|
|
3c0f23e3f2 | ||
|
|
250b6dbb33 | ||
|
|
be945e010a | ||
|
|
9308127342 | ||
|
|
141edb881e | ||
|
|
20a1e3160b | ||
|
|
d4c458d193 | ||
|
|
ed87e703ff | ||
|
|
dcb5725642 | ||
|
|
f5d97823e8 | ||
|
|
d6083dab6e | ||
|
|
6b3b4d83ff | ||
|
|
3853c3318f | ||
|
|
257ccc5f43 | ||
|
|
cec5fb0d6c | ||
|
|
3fc4313e89 | ||
|
|
c4c99b7f75 | ||
|
|
a984bbbc7a | ||
|
|
ba067667c9 | ||
|
|
e38a690632 | ||
|
|
7d0656f44a | ||
|
|
11f33ff8b6 | ||
|
|
611363fca7 | ||
|
|
85d43d2366 | ||
|
|
8faaa3cf91 | ||
|
|
20462c52a5 | ||
|
|
52b77e4194 | ||
|
|
010ba0d15c | ||
|
|
9b7fac5147 | ||
|
|
be12c12b28 | ||
|
|
a19a643c65 | ||
|
|
f9b060af18 | ||
|
|
a3d78e95f2 | ||
|
|
d85b06332c | ||
|
|
bfa10cab62 | ||
|
|
08fcb430e6 | ||
|
|
5d02df62d0 | ||
|
|
c3a2e084b3 | ||
|
|
4296741ec0 | ||
|
|
6bee4ed147 | ||
|
|
e62c3edc1c | ||
|
|
0a08d0af3b | ||
|
|
ad513354b9 | ||
|
|
a70b81f931 |
@@ -4,10 +4,10 @@
|
||||
"dockerfile": "Dockerfile",
|
||||
"args": {
|
||||
// Update the VARIANT arg to pick a version of Go: 1, 1.15, 1.14
|
||||
"VARIANT": "1.19",
|
||||
"VARIANT": "1.20",
|
||||
// Options
|
||||
"INSTALL_NODE": "true",
|
||||
"NODE_VERSION": "v16"
|
||||
"NODE_VERSION": "v18"
|
||||
}
|
||||
},
|
||||
"workspaceMount": "",
|
||||
|
||||
27
.github/workflows/pipeline.yml
vendored
27
.github/workflows/pipeline.yml
vendored
@@ -8,6 +8,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
go-lint:
|
||||
name: Lint Go code
|
||||
@@ -43,6 +44,28 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Build and Lint OpenAPI spec
|
||||
run: make lintapi gen
|
||||
|
||||
- name: Verify no changes
|
||||
run: |
|
||||
git status --porcelain
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo 'Changes to OpenAPI spec caused changes to the code. Please review and commit the changes.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: openapi.yaml
|
||||
path: api/openapi.yaml
|
||||
|
||||
go:
|
||||
name: Test with Go ${{ matrix.go_version }}
|
||||
runs-on: ubuntu-latest
|
||||
@@ -80,7 +103,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
@@ -108,7 +131,6 @@ jobs:
|
||||
with:
|
||||
name: js-bundle
|
||||
path: ui/build
|
||||
retention-days: 7
|
||||
|
||||
binaries:
|
||||
name: Build binaries
|
||||
@@ -155,7 +177,6 @@ jobs:
|
||||
dist
|
||||
!dist/*.tar.gz
|
||||
!dist/*.zip
|
||||
retention-days: 7
|
||||
|
||||
docker:
|
||||
name: Build and publish Docker images
|
||||
|
||||
1
.github/workflows/update-translations.yml
vendored
1
.github/workflows/update-translations.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
jobs:
|
||||
update-translations:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'navidrome' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Get updated translations
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -24,4 +24,5 @@ navidrome.db-wal
|
||||
tags
|
||||
.gitinfo
|
||||
docker-compose.yml
|
||||
!contrib/docker-compose.yml
|
||||
!contrib/docker-compose.yml
|
||||
/api/openapi.yaml
|
||||
|
||||
@@ -7,7 +7,6 @@ linters:
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- depguard
|
||||
- dogsled
|
||||
- durationcheck
|
||||
- errcheck
|
||||
|
||||
25
Makefile
25
Makefile
@@ -32,7 +32,7 @@ test: ##@Development Run Go tests
|
||||
go test -race -shuffle=on ./...
|
||||
.PHONY: test
|
||||
|
||||
testall: test ##@Development Run Go and JS tests
|
||||
testall: test ##@Development Run Go and JS tests, and validate OpenAPI spec
|
||||
@(cd ./ui && npm test -- --watchAll=false)
|
||||
.PHONY: testall
|
||||
|
||||
@@ -40,15 +40,28 @@ lint: ##@Development Lint Go code
|
||||
go run github.com/golangci/golangci-lint/cmd/golangci-lint@latest run -v --timeout 5m
|
||||
.PHONY: lint
|
||||
|
||||
lintall: lint ##@Development Lint Go and JS code
|
||||
lintapi: api/openapi.yaml ##@Development Lint OpenAPI spec
|
||||
npx @redocly/cli lint api/openapi.yaml
|
||||
.PHONY: lintapi
|
||||
|
||||
lintall: lint lintapi ##@Development Lint Go and JS code
|
||||
@(cd ./ui && npm run check-formatting) || (echo "\n\nPlease run 'npm run prettier' to fix formatting issues." && exit 1)
|
||||
@(cd ./ui && npm run lint)
|
||||
.PHONY: lintall
|
||||
|
||||
wire: check_go_env ##@Development Update Dependency Injection
|
||||
gen: check_go_env api ##@Development Update Generated Code (wire, mocks, openapi, etc)
|
||||
go run github.com/google/wire/cmd/wire@latest ./...
|
||||
.PHONY: wire
|
||||
|
||||
api: check_go_env api/openapi.yaml
|
||||
go generate ./server/api/...
|
||||
.PHONY: api
|
||||
|
||||
spec_parts=$(shell find api -name '*.yml')
|
||||
api/openapi.yaml: $(spec_parts)
|
||||
@echo "Bundling OpenAPI spec..."
|
||||
npx @redocly/cli bundle api/spec.yml -o api/openapi.yaml
|
||||
|
||||
snapshots: ##@Development Update (GoLang) Snapshot tests
|
||||
UPDATE_SNAPSHOTS=true go run github.com/onsi/ginkgo/v2/ginkgo@latest ./server/subsonic/...
|
||||
.PHONY: snapshots
|
||||
@@ -108,9 +121,9 @@ get-music: ##@Development Download some free music from Navidrome's demo instanc
|
||||
mkdir -p music
|
||||
( cd music; \
|
||||
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=dev_download&id=ec2093ec4801402f1e17cc462195cdbb" > brock.zip; \
|
||||
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=NavidromeUI&id=b376eeb4652d2498aa2b25ba0696725e" > back_on_earth.zip; \
|
||||
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=NavidromeUI&id=e49c609b542fc51899ee8b53aa858cb4" > ugress.zip; \
|
||||
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=NavidromeUI&id=350bcab3a4c1d93869e39ce496464f03" > voodoocuts.zip; \
|
||||
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=dev_download&id=b376eeb4652d2498aa2b25ba0696725e" > back_on_earth.zip; \
|
||||
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=dev_download&id=e49c609b542fc51899ee8b53aa858cb4" > ugress.zip; \
|
||||
curl "https://demo.navidrome.org/rest/download?u=demo&p=demo&f=json&v=1.8.0&c=dev_download&id=350bcab3a4c1d93869e39ce496464f03" > voodoocuts.zip; \
|
||||
for file in *.zip; do unzip -n $${file}; done )
|
||||
@echo "Done. Remember to set your MusicFolder to ./music"
|
||||
.PHONY: get-music
|
||||
|
||||
28
api/parameters/_index.yml
Normal file
28
api/parameters/_index.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
pageOffset:
|
||||
$ref: './query/pageOffset.yml'
|
||||
pageLimit:
|
||||
$ref: './query/pageLimit.yml'
|
||||
filterEquals:
|
||||
$ref: './query/filterEquals.yml'
|
||||
filterLessThan:
|
||||
$ref: './query/filterLessThan.yml'
|
||||
filterLessOrEqual:
|
||||
$ref: './query/filterLessOrEqual.yml'
|
||||
filterGreaterThan:
|
||||
$ref: './query/filterGreaterThan.yml'
|
||||
filterGreaterOrEqual:
|
||||
$ref: './query/filterGreaterOrEqual.yml'
|
||||
filterContains:
|
||||
$ref: './query/filterContains.yml'
|
||||
filterStartsWith:
|
||||
$ref: './query/filterStartsWith.yml'
|
||||
filterEndsWith:
|
||||
$ref: './query/filterEndsWith.yml'
|
||||
sort:
|
||||
$ref: './query/sort.yml'
|
||||
include:
|
||||
$ref: './query/include.yml'
|
||||
includeForTracks:
|
||||
$ref: './query/includeForTracks.yml'
|
||||
includeForAlbums:
|
||||
$ref: './query/includeForAlbums.yml'
|
||||
9
api/parameters/query/filterContains.yml
Normal file
9
api/parameters/query/filterContains.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
name: filter[contains]
|
||||
in: query
|
||||
description: 'Filter by any property containing text. Usage: filter[contains]=property:value'
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: '^\w+:\w+'
|
||||
9
api/parameters/query/filterEndsWith.yml
Normal file
9
api/parameters/query/filterEndsWith.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
name: filter[endsWith]
|
||||
in: query
|
||||
description: 'Filter by any property that ends with text. Usage: filter[endsWith]=property:value'
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: '^\w+:\w+'
|
||||
9
api/parameters/query/filterEquals.yml
Normal file
9
api/parameters/query/filterEquals.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
name: filter[equals]
|
||||
in: query
|
||||
description: 'Filter by any property with an exact match. Usage: filter[equals]=property:value'
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: '^\w+:\w+'
|
||||
9
api/parameters/query/filterGreaterOrEqual.yml
Normal file
9
api/parameters/query/filterGreaterOrEqual.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
name: filter[greaterOrEqual]
|
||||
in: query
|
||||
description: 'Filter by any numeric property greater than or equal to a value. Usage: filter[greaterOrEqual]=property:value'
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: '^\w+:\d+'
|
||||
9
api/parameters/query/filterGreaterThan.yml
Normal file
9
api/parameters/query/filterGreaterThan.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
name: filter[greaterThan]
|
||||
in: query
|
||||
description: 'Filter by any numeric property greater than a value. Usage: filter[greaterThan]=property:value'
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: '^\w+:\d+'
|
||||
9
api/parameters/query/filterLessOrEqual.yml
Normal file
9
api/parameters/query/filterLessOrEqual.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
name: filter[lessOrEqual]
|
||||
in: query
|
||||
description: 'Filter by any numeric property less than or equal to a value. Usage: filter[lessOrEqual]=property:value'
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: '^\w+:\d+'
|
||||
9
api/parameters/query/filterLessThan.yml
Normal file
9
api/parameters/query/filterLessThan.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
name: filter[lessThan]
|
||||
in: query
|
||||
description: 'Filter by any numeric property less than a value. Usage: filter[lessThan]=property:value'
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: '^\w+:\d+'
|
||||
9
api/parameters/query/filterStartsWith.yml
Normal file
9
api/parameters/query/filterStartsWith.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
name: filter[startsWith]
|
||||
in: query
|
||||
description: 'Filter by any property that starts with text. Usage: filter[startsWith]=property:value'
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: '^\w+:\w+'
|
||||
6
api/parameters/query/include.yml
Normal file
6
api/parameters/query/include.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
name: include
|
||||
in: query
|
||||
description: Related resources to include in the response, separated by commas
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
13
api/parameters/query/includeForAlbum.yml
Normal file
13
api/parameters/query/includeForAlbum.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
name: include
|
||||
in: query
|
||||
description: Related resources to include in the response, separated by commas
|
||||
required: false
|
||||
explode: false
|
||||
schema:
|
||||
type: array
|
||||
x-go-type: includeSlice
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- track
|
||||
- artist
|
||||
12
api/parameters/query/includeForAlbums.yml
Normal file
12
api/parameters/query/includeForAlbums.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
name: include
|
||||
in: query
|
||||
description: Related resources to include in the response, separated by commas
|
||||
required: false
|
||||
explode: false
|
||||
schema:
|
||||
type: array
|
||||
x-go-type: includeSlice
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- artist
|
||||
13
api/parameters/query/includeForTracks.yml
Normal file
13
api/parameters/query/includeForTracks.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
name: include
|
||||
in: query
|
||||
description: Related resources to include in the response, separated by commas
|
||||
required: false
|
||||
explode: false
|
||||
schema:
|
||||
type: array
|
||||
x-go-type: includeSlice
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- album
|
||||
- artist
|
||||
9
api/parameters/query/pageLimit.yml
Normal file
9
api/parameters/query/pageLimit.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
name: page[limit]
|
||||
in: query
|
||||
description: The number of items per page
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 0
|
||||
default: 10
|
||||
9
api/parameters/query/pageOffset.yml
Normal file
9
api/parameters/query/pageOffset.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
name: page[offset]
|
||||
in: query
|
||||
description: The offset for pagination
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 0
|
||||
default: 0
|
||||
6
api/parameters/query/sort.yml
Normal file
6
api/parameters/query/sort.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
name: sort
|
||||
in: query
|
||||
description: Sort the results by one or more properties, separated by commas. Prefix the property with '-' for descending order.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
33
api/resources/album.yml
Normal file
33
api/resources/album.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
get:
|
||||
summary: Retrieve an individual album
|
||||
operationId: getAlbum
|
||||
parameters:
|
||||
- $ref: '../parameters/query/includeForAlbum.yml'
|
||||
- name: albumId
|
||||
in: path
|
||||
description: The unique identifier of the album
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: An album object
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
type: object
|
||||
required: [data]
|
||||
properties:
|
||||
data:
|
||||
$ref: '../schemas/Album.yml'
|
||||
included:
|
||||
description: Included resources, as requested by the `include` query parameter
|
||||
type: array
|
||||
items:
|
||||
$ref: '../schemas/IncludedResource.yml'
|
||||
'403':
|
||||
$ref: '../responses/NotAuthorized.yml'
|
||||
'404':
|
||||
$ref: '../responses/NotFound.yml'
|
||||
'500':
|
||||
$ref: '../responses/InternalServerError.yml'
|
||||
44
api/resources/albums.yml
Normal file
44
api/resources/albums.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
get:
|
||||
summary: Retrieve a list of albums
|
||||
operationId: getAlbums
|
||||
parameters:
|
||||
- $ref: '../parameters/query/pageLimit.yml'
|
||||
- $ref: '../parameters/query/pageOffset.yml'
|
||||
- $ref: '../parameters/query/filterEquals.yml'
|
||||
- $ref: '../parameters/query/filterContains.yml'
|
||||
- $ref: '../parameters/query/filterLessThan.yml'
|
||||
- $ref: '../parameters/query/filterLessOrEqual.yml'
|
||||
- $ref: '../parameters/query/filterGreaterThan.yml'
|
||||
- $ref: '../parameters/query/filterGreaterOrEqual.yml'
|
||||
- $ref: '../parameters/query/filterStartsWith.yml'
|
||||
- $ref: '../parameters/query/filterEndsWith.yml'
|
||||
- $ref: '../parameters/query/sort.yml'
|
||||
- $ref: '../parameters/query/includeForAlbums.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: A list of albums
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
type: object
|
||||
required: [data, links]
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../schemas/Album.yml'
|
||||
links:
|
||||
$ref: '../schemas/PaginationLinks.yml'
|
||||
meta:
|
||||
$ref: '../schemas/PaginationMeta.yml'
|
||||
included:
|
||||
description: Included resources, as requested by the `include` query parameter
|
||||
type: array
|
||||
items:
|
||||
$ref: '../schemas/IncludedResource.yml'
|
||||
'400':
|
||||
$ref: '../responses/BadRequest.yml'
|
||||
'403':
|
||||
$ref: '../responses/NotAuthorized.yml'
|
||||
'500':
|
||||
$ref: '../responses/InternalServerError.yml'
|
||||
28
api/resources/artist.yml
Normal file
28
api/resources/artist.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
get:
|
||||
summary: Retrieve an individual artist
|
||||
operationId: getArtist
|
||||
parameters:
|
||||
- $ref: '../parameters/query/include.yml'
|
||||
- name: artistId
|
||||
in: path
|
||||
description: The unique identifier of the artist
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: An artist object
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
type: object
|
||||
required: [data]
|
||||
properties:
|
||||
data:
|
||||
$ref: '../schemas/Artist.yml'
|
||||
'403':
|
||||
$ref: '../responses/NotAuthorized.yml'
|
||||
'404':
|
||||
$ref: '../responses/NotFound.yml'
|
||||
'500':
|
||||
$ref: '../responses/InternalServerError.yml'
|
||||
39
api/resources/artists.yml
Normal file
39
api/resources/artists.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
get:
|
||||
summary: Retrieve a list of artists
|
||||
operationId: getArtists
|
||||
parameters:
|
||||
- $ref: '../parameters/query/pageLimit.yml'
|
||||
- $ref: '../parameters/query/pageOffset.yml'
|
||||
- $ref: '../parameters/query/filterEquals.yml'
|
||||
- $ref: '../parameters/query/filterContains.yml'
|
||||
- $ref: '../parameters/query/filterLessThan.yml'
|
||||
- $ref: '../parameters/query/filterLessOrEqual.yml'
|
||||
- $ref: '../parameters/query/filterGreaterThan.yml'
|
||||
- $ref: '../parameters/query/filterGreaterOrEqual.yml'
|
||||
- $ref: '../parameters/query/filterStartsWith.yml'
|
||||
- $ref: '../parameters/query/filterEndsWith.yml'
|
||||
- $ref: '../parameters/query/sort.yml'
|
||||
- $ref: '../parameters/query/include.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: A list of artists
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
type: object
|
||||
required: [data, links]
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../schemas/Artist.yml'
|
||||
links:
|
||||
$ref: '../schemas/PaginationLinks.yml'
|
||||
meta:
|
||||
$ref: '../schemas/PaginationMeta.yml'
|
||||
'400':
|
||||
$ref: '../responses/BadRequest.yml'
|
||||
'403':
|
||||
$ref: '../responses/NotAuthorized.yml'
|
||||
'500':
|
||||
$ref: '../responses/InternalServerError.yml'
|
||||
18
api/resources/server.yml
Normal file
18
api/resources/server.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
get:
|
||||
summary: Get server's global info
|
||||
operationId: getServerInfo
|
||||
responses:
|
||||
'200':
|
||||
description: The response’s data key maps to a resource object dictionary representing the server.
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
type: object
|
||||
required: [data]
|
||||
properties:
|
||||
data:
|
||||
$ref: '../schemas/ServerInfo.yml'
|
||||
'403':
|
||||
$ref: '../responses/NotAuthorized.yml'
|
||||
'500':
|
||||
$ref: '../responses/InternalServerError.yml'
|
||||
33
api/resources/track.yml
Normal file
33
api/resources/track.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
get:
|
||||
summary: Retrieve an individual track
|
||||
operationId: getTrack
|
||||
parameters:
|
||||
- $ref: '../parameters/query/includeForTracks.yml'
|
||||
- name: trackId
|
||||
in: path
|
||||
description: The unique identifier of the track
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: A track object
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
type: object
|
||||
required: [data]
|
||||
properties:
|
||||
data:
|
||||
$ref: '../schemas/Track.yml'
|
||||
included:
|
||||
description: Included resources, as requested by the `include` query parameter
|
||||
type: array
|
||||
items:
|
||||
$ref: '../schemas/IncludedResource.yml'
|
||||
'403':
|
||||
$ref: '../responses/NotAuthorized.yml'
|
||||
'404':
|
||||
$ref: '../responses/NotFound.yml'
|
||||
'500':
|
||||
$ref: '../responses/InternalServerError.yml'
|
||||
44
api/resources/tracks.yml
Normal file
44
api/resources/tracks.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
get:
|
||||
summary: Retrieve a list of tracks
|
||||
operationId: getTracks
|
||||
parameters:
|
||||
- $ref: '../parameters/query/pageLimit.yml'
|
||||
- $ref: '../parameters/query/pageOffset.yml'
|
||||
- $ref: '../parameters/query/filterEquals.yml'
|
||||
- $ref: '../parameters/query/filterContains.yml'
|
||||
- $ref: '../parameters/query/filterLessThan.yml'
|
||||
- $ref: '../parameters/query/filterLessOrEqual.yml'
|
||||
- $ref: '../parameters/query/filterGreaterThan.yml'
|
||||
- $ref: '../parameters/query/filterGreaterOrEqual.yml'
|
||||
- $ref: '../parameters/query/filterStartsWith.yml'
|
||||
- $ref: '../parameters/query/filterEndsWith.yml'
|
||||
- $ref: '../parameters/query/sort.yml'
|
||||
- $ref: '../parameters/query/includeForTracks.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: A list of tracks
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
type: object
|
||||
required: [data, links]
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../schemas/Track.yml'
|
||||
links:
|
||||
$ref: '../schemas/PaginationLinks.yml'
|
||||
meta:
|
||||
$ref: '../schemas/PaginationMeta.yml'
|
||||
included:
|
||||
description: Included resources, as requested by the `include` query parameter
|
||||
type: array
|
||||
items:
|
||||
$ref: '../schemas/IncludedResource.yml'
|
||||
'400':
|
||||
$ref: '../responses/BadRequest.yml'
|
||||
'403':
|
||||
$ref: '../responses/NotAuthorized.yml'
|
||||
'500':
|
||||
$ref: '../responses/InternalServerError.yml'
|
||||
5
api/responses/BadRequest.yml
Normal file
5
api/responses/BadRequest.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
description: Bad Request
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '../schemas/ErrorList.yml'
|
||||
5
api/responses/InternalServerError.yml
Normal file
5
api/responses/InternalServerError.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '../schemas/ErrorList.yml'
|
||||
5
api/responses/NotAuthorized.yml
Normal file
5
api/responses/NotAuthorized.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
description: Not Authorized
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '../schemas/ErrorList.yml'
|
||||
5
api/responses/NotFound.yml
Normal file
5
api/responses/NotFound.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
description: Not Found
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '../schemas/ErrorList.yml'
|
||||
8
api/responses/_index.yml
Normal file
8
api/responses/_index.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
NotFound:
|
||||
$ref: './NotFound.yml'
|
||||
NotAuthorized:
|
||||
$ref: './NotAuthorized.yml'
|
||||
BadRequest:
|
||||
$ref: './BadRequest.yml'
|
||||
InternalServerError:
|
||||
$ref: './InternalServerError.yml'
|
||||
20
api/schemas/Album.yml
Normal file
20
api/schemas/Album.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
allOf:
|
||||
- $ref: './ResourceObject.yml'
|
||||
- type: object
|
||||
properties:
|
||||
attributes:
|
||||
$ref: './AlbumAttributes.yml'
|
||||
relationships:
|
||||
type: object
|
||||
properties:
|
||||
artists:
|
||||
type: array
|
||||
items:
|
||||
$ref: './AlbumArtistRelationship.yml'
|
||||
tracks:
|
||||
type: array
|
||||
items:
|
||||
$ref: './AlbumTrackRelationship.yml'
|
||||
required:
|
||||
- artists
|
||||
- tracks
|
||||
9
api/schemas/AlbumArtistRelationship.yml
Normal file
9
api/schemas/AlbumArtistRelationship.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
type: object
|
||||
properties:
|
||||
meta:
|
||||
$ref: './ArtistMetaObject.yml'
|
||||
data:
|
||||
$ref: './ResourceObject.yml'
|
||||
required:
|
||||
- meta
|
||||
- data
|
||||
23
api/schemas/AlbumAttributes.yml
Normal file
23
api/schemas/AlbumAttributes.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description: The title of the album
|
||||
artist:
|
||||
type: string
|
||||
description: The artist of the album
|
||||
releaseDate:
|
||||
type: string
|
||||
description: The release date of the album
|
||||
tracktotal:
|
||||
type: integer
|
||||
description: The number of tracks on the album
|
||||
disctotal:
|
||||
type: integer
|
||||
description: The number of discs in the album
|
||||
genre:
|
||||
type: string
|
||||
description: The genre of the album
|
||||
required:
|
||||
- title
|
||||
- artist
|
||||
6
api/schemas/AlbumTrackRelationship.yml
Normal file
6
api/schemas/AlbumTrackRelationship.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: './ResourceObject.yml'
|
||||
required:
|
||||
- data
|
||||
27
api/schemas/Artist.yml
Normal file
27
api/schemas/Artist.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
allOf:
|
||||
- $ref: './ResourceObject.yml'
|
||||
- type: object
|
||||
properties:
|
||||
attributes:
|
||||
$ref: './ArtistAttributes.yml'
|
||||
relationships:
|
||||
type: object
|
||||
properties:
|
||||
tracks:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: './ArtistTrackRelationship.yml'
|
||||
required:
|
||||
- data
|
||||
albums:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: './ArtistAlbumRelationship.yml'
|
||||
required:
|
||||
- data
|
||||
9
api/schemas/ArtistAlbumRelationship.yml
Normal file
9
api/schemas/ArtistAlbumRelationship.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
type: object
|
||||
properties:
|
||||
meta:
|
||||
$ref: './ArtistMetaObject.yml'
|
||||
data:
|
||||
$ref: './ResourceObject.yml'
|
||||
required:
|
||||
- meta
|
||||
- data
|
||||
10
api/schemas/ArtistAttributes.yml
Normal file
10
api/schemas/ArtistAttributes.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: The name of the artist
|
||||
bio:
|
||||
type: string
|
||||
description: A short biography of the artist
|
||||
required:
|
||||
- name
|
||||
6
api/schemas/ArtistMetaObject.yml
Normal file
6
api/schemas/ArtistMetaObject.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
type: object
|
||||
properties:
|
||||
role:
|
||||
$ref: './ArtistRole.yml'
|
||||
required:
|
||||
- role
|
||||
5
api/schemas/ArtistRole.yml
Normal file
5
api/schemas/ArtistRole.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
type: string
|
||||
enum:
|
||||
- artist
|
||||
- albumArtist
|
||||
description: The role of an artist in a track or album
|
||||
14
api/schemas/ArtistTrackRelationship.yml
Normal file
14
api/schemas/ArtistTrackRelationship.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
type: object
|
||||
properties:
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
role:
|
||||
$ref: './ArtistRole.yml'
|
||||
required:
|
||||
- role
|
||||
data:
|
||||
$ref: './ResourceObject.yml'
|
||||
required:
|
||||
- meta
|
||||
- data
|
||||
7
api/schemas/ErrorList.yml
Normal file
7
api/schemas/ErrorList.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
type: object
|
||||
required: [errors]
|
||||
properties:
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
$ref: './ErrorObject.yml'
|
||||
10
api/schemas/ErrorObject.yml
Normal file
10
api/schemas/ErrorObject.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
detail:
|
||||
type: string
|
||||
10
api/schemas/IncludedResource.yml
Normal file
10
api/schemas/IncludedResource.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
oneOf:
|
||||
- $ref: './Track.yml'
|
||||
- $ref: './Album.yml'
|
||||
- $ref: './Artist.yml'
|
||||
discriminator:
|
||||
propertyName: type
|
||||
mapping:
|
||||
track: './Track.yml'
|
||||
album: './Album.yml'
|
||||
artist: './Artist.yml'
|
||||
14
api/schemas/PaginationLinks.yml
Normal file
14
api/schemas/PaginationLinks.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
type: object
|
||||
properties:
|
||||
first:
|
||||
type: string
|
||||
format: uri
|
||||
prev:
|
||||
type: string
|
||||
format: uri
|
||||
next:
|
||||
type: string
|
||||
format: uri
|
||||
last:
|
||||
type: string
|
||||
format: uri
|
||||
14
api/schemas/PaginationMeta.yml
Normal file
14
api/schemas/PaginationMeta.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
type: object
|
||||
properties:
|
||||
currentPage:
|
||||
type: integer
|
||||
format: int32
|
||||
description: The current page in the collection
|
||||
totalPages:
|
||||
type: integer
|
||||
format: int32
|
||||
description: The total number of pages in the collection
|
||||
totalItems:
|
||||
type: integer
|
||||
format: int32
|
||||
description: The total number of items in the collection
|
||||
18
api/schemas/ResourceList.yml
Normal file
18
api/schemas/ResourceList.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
oneOf:
|
||||
- $ref: './Track.yml'
|
||||
- $ref: './Album.yml'
|
||||
- $ref: './Artist.yml'
|
||||
- type: array
|
||||
items:
|
||||
$ref: './ResourceObject.yml'
|
||||
included:
|
||||
type: array
|
||||
items:
|
||||
$ref: './IncludedResource.yml'
|
||||
links:
|
||||
$ref: './PaginationLinks.yml'
|
||||
meta:
|
||||
$ref: './PaginationMeta.yml'
|
||||
8
api/schemas/ResourceObject.yml
Normal file
8
api/schemas/ResourceObject.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
type: object
|
||||
required: [id, type]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: The unique identifier for the resource
|
||||
type:
|
||||
$ref: './ResourceType.yml'
|
||||
6
api/schemas/ResourceType.yml
Normal file
6
api/schemas/ResourceType.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
type: string
|
||||
description: The type of the resource
|
||||
enum:
|
||||
- album
|
||||
- artist
|
||||
- track
|
||||
24
api/schemas/ServerInfo.yml
Normal file
24
api/schemas/ServerInfo.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
type: object
|
||||
required: [server, serverVersion, authRequired, features]
|
||||
properties:
|
||||
server:
|
||||
type: string
|
||||
description: The name of the server software.
|
||||
example: "navidrome"
|
||||
serverVersion:
|
||||
type: string
|
||||
description: The version number of the server.
|
||||
example: "0.60.0"
|
||||
authRequired:
|
||||
type: boolean
|
||||
description: Whether the user has access to the server.
|
||||
example: true
|
||||
features:
|
||||
type: array
|
||||
description: A list of optional features the server supports.
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- albums
|
||||
- artists
|
||||
- images
|
||||
8
api/schemas/Track.yml
Normal file
8
api/schemas/Track.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
allOf:
|
||||
- $ref: './ResourceObject.yml'
|
||||
- type: object
|
||||
properties:
|
||||
attributes:
|
||||
$ref: './TrackAttributes.yml'
|
||||
relationships:
|
||||
$ref: './TrackRelationships.yml'
|
||||
9
api/schemas/TrackArtistRelationship.yml
Normal file
9
api/schemas/TrackArtistRelationship.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
type: object
|
||||
properties:
|
||||
meta:
|
||||
$ref: './ArtistMetaObject.yml'
|
||||
data:
|
||||
$ref: './ResourceObject.yml'
|
||||
required:
|
||||
- meta
|
||||
- data
|
||||
55
api/schemas/TrackAttributes.yml
Normal file
55
api/schemas/TrackAttributes.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
type: object
|
||||
required: [title, artist, album, albumartist, track, mimetype, duration, channels, bitrate, size]
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description: The title of the track
|
||||
artist: # TODO: Remove
|
||||
type: string
|
||||
description: The name of the artist who performed the track
|
||||
albumartist: # TODO: Remove
|
||||
type: string
|
||||
description: The primary artist of the album the track belongs to.
|
||||
album: # TODO Remove
|
||||
type: string
|
||||
description: The name of the album the track belongs to
|
||||
genre: # TODO Remove
|
||||
type: string
|
||||
description: The genre of the track.
|
||||
track:
|
||||
type: integer
|
||||
description: The track number within the album.
|
||||
disc:
|
||||
type: integer
|
||||
description: The disc number within a multi-disc album.
|
||||
year:
|
||||
type: integer
|
||||
description: The release year of the track or album.
|
||||
bpm:
|
||||
type: integer
|
||||
description: The beats per minute (BPM) of the track.
|
||||
recording-mbid:
|
||||
type: string
|
||||
description: The MusicBrainz identifier for the recording of the track.
|
||||
track-mbid:
|
||||
type: string
|
||||
description: The MusicBrainz identifier for the track.
|
||||
comments:
|
||||
type: string
|
||||
description: Any additional comments or notes about the track.
|
||||
mimetype:
|
||||
type: string
|
||||
description: The MIME type of the audio file.
|
||||
duration:
|
||||
type: number
|
||||
format: float
|
||||
description: The duration of the track in seconds
|
||||
channels:
|
||||
type: integer
|
||||
description: The number of audio channels in the track.
|
||||
bitrate:
|
||||
type: integer
|
||||
description: The bitrate of the audio file in kilobits per second (kbps).
|
||||
size:
|
||||
type: integer
|
||||
description: The size of the audio file in bytes.
|
||||
12
api/schemas/TrackRelationships.yml
Normal file
12
api/schemas/TrackRelationships.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
type: object
|
||||
properties:
|
||||
artists:
|
||||
type: array
|
||||
items:
|
||||
$ref: './TrackArtistRelationship.yml'
|
||||
albums:
|
||||
type: array
|
||||
items:
|
||||
$ref: './AlbumTrackRelationship.yml'
|
||||
required:
|
||||
- artists
|
||||
46
api/schemas/_index.yml
Normal file
46
api/schemas/_index.yml
Normal file
@@ -0,0 +1,46 @@
|
||||
ServerInfo:
|
||||
$ref: './ServerInfo.yml'
|
||||
ResourceObject:
|
||||
$ref: './ResourceObject.yml'
|
||||
ResourceType:
|
||||
$ref: './ResourceType.yml'
|
||||
ResourceList:
|
||||
$ref: './ResourceList.yml'
|
||||
IncludedResource:
|
||||
$ref: './IncludedResource.yml'
|
||||
Track:
|
||||
$ref: './Track.yml'
|
||||
TrackAttributes:
|
||||
$ref: './TrackAttributes.yml'
|
||||
TrackRelationships:
|
||||
$ref: './TrackRelationships.yml'
|
||||
TrackArtistRelationship:
|
||||
$ref: './TrackArtistRelationship.yml'
|
||||
ArtistRole:
|
||||
$ref: './ArtistRole.yml'
|
||||
Artist:
|
||||
$ref: './Artist.yml'
|
||||
ArtistAttributes:
|
||||
$ref: './ArtistAttributes.yml'
|
||||
ArtistAlbumRelationship:
|
||||
$ref: './ArtistAlbumRelationship.yml'
|
||||
ArtistTrackRelationship:
|
||||
$ref: './ArtistTrackRelationship.yml'
|
||||
ArtistMetaObject:
|
||||
$ref: './ArtistMetaObject.yml'
|
||||
Album:
|
||||
$ref: './Album.yml'
|
||||
AlbumAttributes:
|
||||
$ref: './AlbumAttributes.yml'
|
||||
AlbumArtistRelationship:
|
||||
$ref: './AlbumArtistRelationship.yml'
|
||||
AlbumTrackRelationship:
|
||||
$ref: './AlbumTrackRelationship.yml'
|
||||
PaginationLinks:
|
||||
$ref: './PaginationLinks.yml'
|
||||
PaginationMeta:
|
||||
$ref: './PaginationMeta.yml'
|
||||
ErrorList:
|
||||
$ref: './ErrorList.yml'
|
||||
ErrorObject:
|
||||
$ref: './ErrorObject.yml'
|
||||
52
api/spec.yml
Normal file
52
api/spec.yml
Normal file
@@ -0,0 +1,52 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
version: 0.2.0
|
||||
title: Navidrome API
|
||||
description: >
|
||||
This spec describes the Navidrome API, which allows users to browse and manage their music library via a JSON:API
|
||||
based interface. The API provides endpoints for albums, tracks, artists, playlists and images, along with their
|
||||
relationships. Clients can retrieve information about the items in the library, filter and sort results, and
|
||||
perform actions such as creating and deleting playlists. With this API, developers can build music apps and
|
||||
services that integrate with Navidrome music server, providing a seamless experience for users to access and
|
||||
manage their music collection.
|
||||
contact:
|
||||
name: Navidrome
|
||||
url: https://navidrome.org
|
||||
license:
|
||||
name: GNU General Public License v3.0
|
||||
url: https://github.com/navidrome/navidrome/blob/master/LICENSE
|
||||
|
||||
servers:
|
||||
- url: /api/v2
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
|
||||
paths:
|
||||
/server:
|
||||
$ref: './resources/server.yml'
|
||||
/tracks:
|
||||
$ref: './resources/tracks.yml'
|
||||
/tracks/{trackId}:
|
||||
$ref: './resources/track.yml'
|
||||
/artists:
|
||||
$ref: './resources/artists.yml'
|
||||
/artists/{artistId}:
|
||||
$ref: './resources/artist.yml'
|
||||
/albums:
|
||||
$ref: './resources/albums.yml'
|
||||
/albums/{albumId}:
|
||||
$ref: './resources/album.yml'
|
||||
|
||||
components:
|
||||
parameters:
|
||||
$ref: './parameters/_index.yml'
|
||||
schemas:
|
||||
$ref: './schemas/_index.yml'
|
||||
responses:
|
||||
$ref: './responses/_index.yml'
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
@@ -27,10 +27,9 @@ func init() {
|
||||
}
|
||||
|
||||
var plsCmd = &cobra.Command{
|
||||
Use: "playlists",
|
||||
Aliases: []string{"pls", "playlist"},
|
||||
Short: "Export playlists",
|
||||
Long: "Export Navidrome playlists to M3U files",
|
||||
Use: "pls",
|
||||
Short: "Export playlists",
|
||||
Long: "Export Navidrome playlists to M3U files",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runExporter()
|
||||
},
|
||||
|
||||
11
cmd/root.go
11
cmd/root.go
@@ -39,7 +39,7 @@ Complete documentation is available at https://www.navidrome.org/docs`,
|
||||
preRun()
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runNavidrome(context.Background())
|
||||
runNavidrome()
|
||||
},
|
||||
Version: consts.Version,
|
||||
}
|
||||
@@ -60,7 +60,7 @@ func preRun() {
|
||||
conf.Load()
|
||||
}
|
||||
|
||||
func runNavidrome(ctx context.Context) {
|
||||
func runNavidrome() {
|
||||
db.Init()
|
||||
defer func() {
|
||||
if err := db.Close(); err != nil {
|
||||
@@ -69,7 +69,7 @@ func runNavidrome(ctx context.Context) {
|
||||
log.Info("Navidrome stopped, bye.")
|
||||
}()
|
||||
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
g, ctx := errgroup.WithContext(context.Background())
|
||||
g.Go(startServer(ctx))
|
||||
g.Go(startSignaler(ctx))
|
||||
g.Go(startScheduler(ctx))
|
||||
@@ -103,6 +103,7 @@ func startServer(ctx context.Context) func() error {
|
||||
if strings.HasPrefix(conf.Server.UILoginBackgroundURL, "/") {
|
||||
a.MountRouter("Background images", consts.DefaultUILoginBackgroundURL, backgrounds.NewHandler())
|
||||
}
|
||||
a.MountRouter("New Native API", consts.URLPathAPI, CreateNewNativeAPIRouter())
|
||||
return a.Run(ctx, conf.Server.Address, conf.Server.Port, conf.Server.TLSCert, conf.Server.TLSKey)
|
||||
}
|
||||
}
|
||||
@@ -155,11 +156,13 @@ func init() {
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "configfile", "c", "", `config file (default "./navidrome.toml")`)
|
||||
rootCmd.PersistentFlags().BoolVarP(&noBanner, "nobanner", "n", false, `don't show banner`)
|
||||
rootCmd.PersistentFlags().String("musicfolder", viper.GetString("musicfolder"), "folder where your music is stored")
|
||||
rootCmd.PersistentFlags().String("datafolder", viper.GetString("datafolder"), "folder to store application data (DB, cache...), needs write access")
|
||||
rootCmd.PersistentFlags().String("datafolder", viper.GetString("datafolder"), "folder to store application data (DB), needs write access")
|
||||
rootCmd.PersistentFlags().String("cachefolder", viper.GetString("cachefolder"), "folder to store cache data (transcoding, images...), needs write access")
|
||||
rootCmd.PersistentFlags().StringP("loglevel", "l", viper.GetString("loglevel"), "log level, possible values: error, info, debug, trace")
|
||||
|
||||
_ = viper.BindPFlag("musicfolder", rootCmd.PersistentFlags().Lookup("musicfolder"))
|
||||
_ = viper.BindPFlag("datafolder", rootCmd.PersistentFlags().Lookup("datafolder"))
|
||||
_ = viper.BindPFlag("cachefolder", rootCmd.PersistentFlags().Lookup("cachefolder"))
|
||||
_ = viper.BindPFlag("loglevel", rootCmd.PersistentFlags().Lookup("loglevel"))
|
||||
|
||||
rootCmd.Flags().StringP("address", "a", viper.GetString("address"), "IP address to bind to")
|
||||
|
||||
191
cmd/svc.go
191
cmd/svc.go
@@ -1,191 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
svcStatusLabels = map[service.Status]string{
|
||||
service.StatusUnknown: "Unknown",
|
||||
service.StatusStopped: "Stopped",
|
||||
service.StatusRunning: "Running",
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
svcCmd.AddCommand(buildInstallCmd())
|
||||
svcCmd.AddCommand(buildUninstallCmd())
|
||||
svcCmd.AddCommand(buildStartCmd())
|
||||
svcCmd.AddCommand(buildStopCmd())
|
||||
svcCmd.AddCommand(buildStatusCmd())
|
||||
rootCmd.AddCommand(svcCmd)
|
||||
}
|
||||
|
||||
var svcCmd = &cobra.Command{
|
||||
Use: "service",
|
||||
Aliases: []string{"svc"},
|
||||
Short: "Manage Navidrome as a service",
|
||||
Long: fmt.Sprintf("Manage Navidrome as a service, using the OS service manager (%s)", service.Platform()),
|
||||
Run: runServiceCmd,
|
||||
}
|
||||
|
||||
type svcControl struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func (p *svcControl) Start(_ service.Service) error {
|
||||
p.ctx, p.cancel = context.WithCancel(context.Background())
|
||||
go p.run()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *svcControl) run() {
|
||||
runNavidrome(p.ctx)
|
||||
}
|
||||
|
||||
func (p *svcControl) Stop(_ service.Service) error {
|
||||
log.Info("Stopping service")
|
||||
p.cancel()
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
svc service.Service
|
||||
svcOnce = sync.Once{}
|
||||
)
|
||||
|
||||
func svcInstance() service.Service {
|
||||
svcOnce.Do(func() {
|
||||
options := make(service.KeyValue)
|
||||
options["Restart"] = "on-success"
|
||||
options["SuccessExitStatus"] = "1 2 8 SIGKILL"
|
||||
options["UserService"] = true
|
||||
options["LogDirectory"] = conf.Server.DataFolder
|
||||
svcConfig := &service.Config{
|
||||
Name: "Navidrome",
|
||||
DisplayName: "Navidrome",
|
||||
Description: "Navidrome is a self-hosted music server and streamer",
|
||||
Dependencies: []string{
|
||||
"Requires=network.target",
|
||||
"After=network-online.target syslog.target"},
|
||||
WorkingDirectory: executablePath(),
|
||||
Option: options,
|
||||
}
|
||||
if conf.Server.ConfigFile != "" {
|
||||
svcConfig.Arguments = []string{"-c", conf.Server.ConfigFile}
|
||||
}
|
||||
prg := &svcControl{}
|
||||
var err error
|
||||
svc, err = service.New(prg, svcConfig)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
})
|
||||
return svc
|
||||
}
|
||||
|
||||
func runServiceCmd(cmd *cobra.Command, _ []string) {
|
||||
_ = cmd.Help()
|
||||
}
|
||||
|
||||
func executablePath() string {
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return filepath.Dir(ex)
|
||||
}
|
||||
|
||||
func buildInstallCmd() *cobra.Command {
|
||||
runInstallCmd := func(_ *cobra.Command, _ []string) {
|
||||
var err error
|
||||
println("Installing service with:")
|
||||
println(" working directory: " + executablePath())
|
||||
println(" music folder: " + conf.Server.MusicFolder)
|
||||
println(" data folder: " + conf.Server.DataFolder)
|
||||
if cfgFile != "" {
|
||||
conf.Server.ConfigFile, err = filepath.Abs(cfgFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
println(" config file: " + conf.Server.ConfigFile)
|
||||
}
|
||||
err = svcInstance().Install()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
println("Service installed. Use 'navidrome svc start' to start it.")
|
||||
}
|
||||
|
||||
return &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Install Navidrome service.",
|
||||
Run: runInstallCmd,
|
||||
}
|
||||
}
|
||||
|
||||
func buildUninstallCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "uninstall",
|
||||
Short: "Uninstall Navidrome service. Does not delete the music or data folders",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := svcInstance().Uninstall()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
println("Service uninstalled. Music and data folders are still intact.")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildStartCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "start",
|
||||
Short: "Start Navidrome service",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := svcInstance().Start()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
println("Service started. Use 'navidrome svc status' to check its status.")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildStopCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "stop",
|
||||
Short: "Stop Navidrome service",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := svcInstance().Stop()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
println("Service stopped. Use 'navidrome svc status' to check its status.")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildStatusCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show Navidrome service status",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
status, err := svcInstance().Status()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Navidrome is %s.\n", svcStatusLabels[status])
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/navidrome/navidrome/persistence"
|
||||
"github.com/navidrome/navidrome/scanner"
|
||||
"github.com/navidrome/navidrome/server"
|
||||
"github.com/navidrome/navidrome/server/api"
|
||||
"github.com/navidrome/navidrome/server/events"
|
||||
"github.com/navidrome/navidrome/server/nativeapi"
|
||||
"github.com/navidrome/navidrome/server/public"
|
||||
@@ -44,6 +45,13 @@ func CreateNativeAPIRouter() *nativeapi.Router {
|
||||
return router
|
||||
}
|
||||
|
||||
func CreateNewNativeAPIRouter() *api.Router {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
router := api.New(dataStore)
|
||||
return router
|
||||
}
|
||||
|
||||
func CreateSubsonicAPIRouter() *subsonic.Router {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
@@ -112,7 +120,7 @@ func createScanner() scanner.Scanner {
|
||||
|
||||
// wire_injectors.go:
|
||||
|
||||
var allProviders = wire.NewSet(core.Set, artwork.Set, subsonic.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, db.Db)
|
||||
var allProviders = wire.NewSet(core.Set, artwork.Set, subsonic.New, nativeapi.New, api.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, db.Db)
|
||||
|
||||
// Scanner must be a Singleton
|
||||
var (
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/navidrome/navidrome/persistence"
|
||||
"github.com/navidrome/navidrome/scanner"
|
||||
"github.com/navidrome/navidrome/server"
|
||||
"github.com/navidrome/navidrome/server/api"
|
||||
"github.com/navidrome/navidrome/server/events"
|
||||
"github.com/navidrome/navidrome/server/nativeapi"
|
||||
"github.com/navidrome/navidrome/server/public"
|
||||
@@ -25,6 +26,7 @@ var allProviders = wire.NewSet(
|
||||
artwork.Set,
|
||||
subsonic.New,
|
||||
nativeapi.New,
|
||||
api.New,
|
||||
public.New,
|
||||
persistence.New,
|
||||
lastfm.NewRouter,
|
||||
@@ -46,6 +48,12 @@ func CreateNativeAPIRouter() *nativeapi.Router {
|
||||
))
|
||||
}
|
||||
|
||||
func CreateNewNativeAPIRouter() *api.Router {
|
||||
panic(wire.Build(
|
||||
allProviders,
|
||||
))
|
||||
}
|
||||
|
||||
func CreateSubsonicAPIRouter() *subsonic.Router {
|
||||
panic(wire.Build(
|
||||
allProviders,
|
||||
|
||||
@@ -23,6 +23,7 @@ type configOptions struct {
|
||||
Port int
|
||||
MusicFolder string
|
||||
DataFolder string
|
||||
CacheFolder string
|
||||
DbPath string
|
||||
LogLevel string
|
||||
ScanInterval time.Duration
|
||||
@@ -101,8 +102,9 @@ type configOptions struct {
|
||||
}
|
||||
|
||||
type scannerOptions struct {
|
||||
Extractor string
|
||||
GenreSeparators string
|
||||
Extractor string
|
||||
GenreSeparators string
|
||||
GroupAlbumReleases bool
|
||||
}
|
||||
|
||||
type lastfmOptions struct {
|
||||
@@ -134,6 +136,11 @@ var (
|
||||
|
||||
func LoadFromFile(confFile string) {
|
||||
viper.SetConfigFile(confFile)
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "FATAL: Error reading config file:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
Load()
|
||||
}
|
||||
|
||||
@@ -148,6 +155,16 @@ func Load() {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating data path:", "path", Server.DataFolder, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if Server.CacheFolder == "" {
|
||||
Server.CacheFolder = filepath.Join(Server.DataFolder, "cache")
|
||||
}
|
||||
err = os.MkdirAll(Server.CacheFolder, os.ModePerm)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "FATAL: Error creating cache path:", "path", Server.CacheFolder, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
Server.ConfigFile = viper.GetViper().ConfigFileUsed()
|
||||
if Server.DbPath == "" {
|
||||
Server.DbPath = filepath.Join(Server.DataFolder, consts.DefaultDbPath)
|
||||
@@ -241,6 +258,7 @@ func AddHook(hook func()) {
|
||||
|
||||
func init() {
|
||||
viper.SetDefault("musicfolder", filepath.Join(".", "music"))
|
||||
viper.SetDefault("cachefolder", "")
|
||||
viper.SetDefault("datafolder", ".")
|
||||
viper.SetDefault("loglevel", "info")
|
||||
viper.SetDefault("address", "0.0.0.0")
|
||||
@@ -297,6 +315,7 @@ func init() {
|
||||
|
||||
viper.SetDefault("scanner.extractor", consts.DefaultScannerExtractor)
|
||||
viper.SetDefault("scanner.genreseparators", ";/,")
|
||||
viper.SetDefault("scanner.groupalbumreleases", false)
|
||||
|
||||
viper.SetDefault("agents", "lastfm,spotify")
|
||||
viper.SetDefault("lastfm.enabled", true)
|
||||
|
||||
@@ -32,6 +32,7 @@ const (
|
||||
|
||||
URLPathUI = "/app"
|
||||
URLPathNativeAPI = "/api"
|
||||
URLPathAPI = "/api/v2"
|
||||
URLPathSubsonicAPI = "/rest"
|
||||
URLPathPublic = "/share"
|
||||
URLPathPublicImages = URLPathPublic + "/img"
|
||||
@@ -71,10 +72,10 @@ const (
|
||||
|
||||
// Cache options
|
||||
const (
|
||||
TranscodingCacheDir = "cache/transcoding"
|
||||
TranscodingCacheDir = "transcoding"
|
||||
DefaultTranscodingCacheMaxItems = 0 // Unlimited
|
||||
|
||||
ImageCacheDir = "cache/images"
|
||||
ImageCacheDir = "images"
|
||||
DefaultImageCacheMaxItems = 0 // Unlimited
|
||||
|
||||
DefaultCacheSize = 100 * 1024 * 1024 // 100MB
|
||||
@@ -94,19 +95,19 @@ var (
|
||||
"name": "mp3 audio",
|
||||
"targetFormat": "mp3",
|
||||
"defaultBitRate": 192,
|
||||
"command": "ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -f mp3 -",
|
||||
"command": "ffmpeg -i %s -map 0:a:0 -b:a %bk -v 0 -f mp3 -",
|
||||
},
|
||||
{
|
||||
"name": "opus audio",
|
||||
"targetFormat": "opus",
|
||||
"defaultBitRate": 128,
|
||||
"command": "ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -c:a libopus -f opus -",
|
||||
"command": "ffmpeg -i %s -map 0:a:0 -b:a %bk -v 0 -c:a libopus -f opus -",
|
||||
},
|
||||
{
|
||||
"name": "aac audio",
|
||||
"targetFormat": "aac",
|
||||
"defaultBitRate": 256,
|
||||
"command": "ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -c:a aac -f adts -",
|
||||
"command": "ffmpeg -i %s -map 0:a:0 -b:a %bk -v 0 -c:a aac -f adts -",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ func (a *archiver) albumFilename(mf model.MediaFile, format string, isMultDisc b
|
||||
if isMultDisc {
|
||||
file = fmt.Sprintf("Disc %02d/%s", mf.DiscNumber, file)
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", mf.Album, file)
|
||||
return fmt.Sprintf("%s/%s", sanitizeName(mf.Album), file)
|
||||
}
|
||||
|
||||
func (a *archiver) ZipShare(ctx context.Context, id string, out io.Writer) error {
|
||||
@@ -130,8 +130,11 @@ func (a *archiver) playlistFilename(mf model.MediaFile, format string, idx int)
|
||||
if format != "" && format != "raw" {
|
||||
ext = format
|
||||
}
|
||||
file := fmt.Sprintf("%02d - %s - %s.%s", idx+1, mf.Artist, mf.Title, ext)
|
||||
return file
|
||||
return fmt.Sprintf("%02d - %s - %s.%s", idx+1, sanitizeName(mf.Artist), sanitizeName(mf.Title), ext)
|
||||
}
|
||||
|
||||
func sanitizeName(target string) string {
|
||||
return strings.ReplaceAll(target, "/", "_")
|
||||
}
|
||||
|
||||
func (a *archiver) addFileToZip(ctx context.Context, z *zip.Writer, mf model.MediaFile, format string, bitrate int, filename string) error {
|
||||
|
||||
@@ -33,8 +33,8 @@ var _ = Describe("Archiver", func() {
|
||||
Context("ZipAlbum", func() {
|
||||
It("zips an album correctly", func() {
|
||||
mfs := model.MediaFiles{
|
||||
{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album 1", DiscNumber: 1},
|
||||
{Path: "test_data/02 - track2.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album 1", DiscNumber: 1},
|
||||
{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album/Promo", DiscNumber: 1},
|
||||
{Path: "test_data/02 - track2.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album/Promo", DiscNumber: 1},
|
||||
}
|
||||
|
||||
mfRepo := &mockMediaFileRepository{}
|
||||
@@ -44,7 +44,7 @@ var _ = Describe("Archiver", func() {
|
||||
}}).Return(mfs, nil)
|
||||
|
||||
ds.On("MediaFile", mock.Anything).Return(mfRepo)
|
||||
ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
|
||||
ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128).Return(io.NopCloser(strings.NewReader("test")), nil).Times(3)
|
||||
|
||||
out := new(bytes.Buffer)
|
||||
err := arch.ZipAlbum(context.Background(), "1", "mp3", 128, out)
|
||||
@@ -54,8 +54,8 @@ var _ = Describe("Archiver", func() {
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
Expect(len(zr.File)).To(Equal(2))
|
||||
Expect(zr.File[0].Name).To(Equal("Album 1/01 - track1.mp3"))
|
||||
Expect(zr.File[1].Name).To(Equal("Album 1/02 - track2.mp3"))
|
||||
Expect(zr.File[0].Name).To(Equal("Album_Promo/01 - track1.mp3"))
|
||||
Expect(zr.File[1].Name).To(Equal("Album_Promo/02 - track2.mp3"))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -123,7 +123,7 @@ var _ = Describe("Archiver", func() {
|
||||
Context("ZipPlaylist", func() {
|
||||
It("zips a playlist correctly", func() {
|
||||
tracks := []model.PlaylistTrack{
|
||||
{MediaFile: model.MediaFile{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album 1", DiscNumber: 1, Artist: "Artist 1", Title: "track1"}},
|
||||
{MediaFile: model.MediaFile{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album 1", DiscNumber: 1, Artist: "AC/DC", Title: "track1"}},
|
||||
{MediaFile: model.MediaFile{Path: "test_data/02 - track2.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album 1", DiscNumber: 1, Artist: "Artist 2", Title: "track2"}},
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ var _ = Describe("Archiver", func() {
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
Expect(len(zr.File)).To(Equal(2))
|
||||
Expect(zr.File[0].Name).To(Equal("01 - Artist 1 - track1.mp3"))
|
||||
Expect(zr.File[0].Name).To(Equal("01 - AC_DC - track1.mp3"))
|
||||
Expect(zr.File[1].Name).To(Equal("02 - Artist 2 - track2.mp3"))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -129,11 +129,11 @@ func (s *Stream) EstimatedContentLength() int {
|
||||
func selectTranscodingOptions(ctx context.Context, ds model.DataStore, mf *model.MediaFile, reqFormat string, reqBitRate int) (format string, bitRate int) {
|
||||
format = "raw"
|
||||
if reqFormat == "raw" {
|
||||
return
|
||||
return format, 0
|
||||
}
|
||||
if reqFormat == mf.Suffix && reqBitRate == 0 {
|
||||
bitRate = mf.BitRate
|
||||
return
|
||||
return format, bitRate
|
||||
}
|
||||
trc, hasDefault := request.TranscodingFrom(ctx)
|
||||
var cFormat string
|
||||
@@ -159,7 +159,7 @@ func selectTranscodingOptions(ctx context.Context, ds model.DataStore, mf *model
|
||||
cBitRate = reqBitRate
|
||||
}
|
||||
if cBitRate == 0 && cFormat == "" {
|
||||
return
|
||||
return format, bitRate
|
||||
}
|
||||
t, err := ds.Transcoding(ctx).FindByFormat(cFormat)
|
||||
if err == nil {
|
||||
@@ -184,22 +184,26 @@ var (
|
||||
|
||||
func GetTranscodingCache() TranscodingCache {
|
||||
onceTranscodingCache.Do(func() {
|
||||
instanceTranscodingCache = cache.NewFileCache("Transcoding", conf.Server.TranscodingCacheSize,
|
||||
consts.TranscodingCacheDir, consts.DefaultTranscodingCacheMaxItems,
|
||||
func(ctx context.Context, arg cache.Item) (io.Reader, error) {
|
||||
job := arg.(*streamJob)
|
||||
t, err := job.ms.ds.Transcoding(ctx).FindByFormat(job.format)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error loading transcoding command", "format", job.format, err)
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
out, err := job.ms.transcoder.Transcode(ctx, t.Command, job.mf.Path, job.bitRate)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error starting transcoder", "id", job.mf.ID, err)
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
return out, nil
|
||||
})
|
||||
instanceTranscodingCache = NewTranscodingCache()
|
||||
})
|
||||
return instanceTranscodingCache
|
||||
}
|
||||
|
||||
func NewTranscodingCache() TranscodingCache {
|
||||
return cache.NewFileCache("Transcoding", conf.Server.TranscodingCacheSize,
|
||||
consts.TranscodingCacheDir, consts.DefaultTranscodingCacheMaxItems,
|
||||
func(ctx context.Context, arg cache.Item) (io.Reader, error) {
|
||||
job := arg.(*streamJob)
|
||||
t, err := job.ms.ds.Transcoding(ctx).FindByFormat(job.format)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error loading transcoding command", "format", job.format, err)
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
out, err := job.ms.transcoder.Transcode(ctx, t.Command, job.mf.Path, job.bitRate)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error starting transcoder", "id", job.mf.ID, err)
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
return out, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
@@ -16,21 +14,10 @@ import (
|
||||
|
||||
var _ = Describe("MediaStreamer", func() {
|
||||
var ds model.DataStore
|
||||
ctx := log.NewContext(context.TODO())
|
||||
ctx := log.NewContext(context.Background())
|
||||
|
||||
BeforeEach(func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
conf.Server.DataFolder, _ = os.MkdirTemp("", "file_caches")
|
||||
conf.Server.TranscodingCacheSize = "100MB"
|
||||
ds = &tests.MockDataStore{MockedTranscoding: &tests.MockTranscodingRepo{}}
|
||||
ds.MediaFile(ctx).(*tests.MockMediaFileRepo).SetData(model.MediaFiles{
|
||||
{ID: "123", Path: "tests/fixtures/test.mp3", Suffix: "mp3", BitRate: 128, Duration: 257.0},
|
||||
})
|
||||
testCache := GetTranscodingCache()
|
||||
Eventually(func() bool { return testCache.Available(context.TODO()) }).Should(BeTrue())
|
||||
})
|
||||
AfterEach(func() {
|
||||
_ = os.RemoveAll(conf.Server.DataFolder)
|
||||
})
|
||||
|
||||
Context("selectTranscodingOptions", func() {
|
||||
|
||||
@@ -23,18 +23,18 @@ var _ = Describe("MediaStreamer", func() {
|
||||
|
||||
BeforeEach(func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
conf.Server.DataFolder, _ = os.MkdirTemp("", "file_caches")
|
||||
conf.Server.CacheFolder, _ = os.MkdirTemp("", "file_caches")
|
||||
conf.Server.TranscodingCacheSize = "100MB"
|
||||
ds = &tests.MockDataStore{MockedTranscoding: &tests.MockTranscodingRepo{}}
|
||||
ds.MediaFile(ctx).(*tests.MockMediaFileRepo).SetData(model.MediaFiles{
|
||||
{ID: "123", Path: "tests/fixtures/test.mp3", Suffix: "mp3", BitRate: 128, Duration: 257.0},
|
||||
})
|
||||
testCache := core.GetTranscodingCache()
|
||||
testCache := core.NewTranscodingCache()
|
||||
Eventually(func() bool { return testCache.Available(context.TODO()) }).Should(BeTrue())
|
||||
streamer = core.NewMediaStreamer(ds, ffmpeg, testCache)
|
||||
})
|
||||
AfterEach(func() {
|
||||
_ = os.RemoveAll(conf.Server.DataFolder)
|
||||
_ = os.RemoveAll(conf.Server.CacheFolder)
|
||||
})
|
||||
|
||||
Context("NewStream", func() {
|
||||
|
||||
49
db/migration/20230515184510_add_release_date.go
Normal file
49
db/migration/20230515184510_add_release_date.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigration(upAddRelRecYear, downAddRelRecYear)
|
||||
}
|
||||
|
||||
func upAddRelRecYear(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
alter table media_file
|
||||
add date varchar(255) default '' not null;
|
||||
alter table media_file
|
||||
add original_year int default 0 not null;
|
||||
alter table media_file
|
||||
add original_date varchar(255) default '' not null;
|
||||
alter table media_file
|
||||
add release_year int default 0 not null;
|
||||
alter table media_file
|
||||
add release_date varchar(255) default '' not null;
|
||||
|
||||
alter table album
|
||||
add date varchar(255) default '' not null;
|
||||
alter table album
|
||||
add min_original_year int default 0 not null;
|
||||
alter table album
|
||||
add max_original_year int default 0 not null;
|
||||
alter table album
|
||||
add original_date varchar(255) default '' not null;
|
||||
alter table album
|
||||
add release_date varchar(255) default '' not null;
|
||||
alter table album
|
||||
add releases integer default 0 not null;
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
notice(tx, "A full rescan needs to be performed to import more tags")
|
||||
return forceFullRescan(tx)
|
||||
}
|
||||
|
||||
func downAddRelRecYear(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
64
go.mod
64
go.mod
@@ -8,6 +8,7 @@ require (
|
||||
github.com/ReneKroon/ttlcache/v2 v2.11.0
|
||||
github.com/beego/beego/v2 v2.0.7
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.8.0
|
||||
github.com/deepmap/oapi-codegen v1.12.5-0.20230513000919-14548c7e7bbe
|
||||
github.com/deluan/rest v0.0.0-20211101235434-380523c4bb47
|
||||
github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1
|
||||
github.com/dhowden/tag v0.0.0-20220618230019-adf36e896086
|
||||
@@ -17,25 +18,25 @@ require (
|
||||
github.com/djherbis/stream v1.4.0
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/getkin/kin-openapi v0.116.0
|
||||
github.com/go-chi/chi/v5 v5.0.8
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-chi/httprate v0.7.4
|
||||
github.com/go-chi/httprate v0.7.1
|
||||
github.com/go-chi/jwtauth/v5 v5.1.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/google/wire v0.5.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/kardianos/service v1.2.2
|
||||
github.com/kr/pretty v0.3.1
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.9
|
||||
github.com/matoous/go-nanoid/v2 v2.0.0
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/mattn/go-zglob v0.0.3
|
||||
github.com/microcosm-cc/bluemonday v1.0.23
|
||||
github.com/mileusna/useragent v1.3.2
|
||||
github.com/onsi/ginkgo/v2 v2.9.4
|
||||
github.com/mileusna/useragent v1.2.1
|
||||
github.com/onsi/ginkgo/v2 v2.9.2
|
||||
github.com/onsi/gomega v1.27.6
|
||||
github.com/pressly/goose/v3 v3.11.2
|
||||
github.com/prometheus/client_golang v1.15.1
|
||||
github.com/pressly/goose/v3 v3.10.0
|
||||
github.com/prometheus/client_golang v1.14.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/cobra v1.7.0
|
||||
@@ -45,47 +46,72 @@ require (
|
||||
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/sync v0.1.0
|
||||
golang.org/x/text v0.9.0
|
||||
golang.org/x/tools v0.9.1
|
||||
golang.org/x/tools v0.8.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/bytedance/sonic v1.8.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.9.0 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/swag v0.21.1 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.2 // 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/pprof v0.0.0-20230323073829-e72429f035bd // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // 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/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/invopop/yaml v0.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/labstack/echo/v4 v4.10.2 // indirect
|
||||
github.com/labstack/gommon v0.4.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 // 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/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.4 // 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/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
|
||||
@@ -95,11 +121,17 @@ 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/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.9 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
go.uber.org/goleak v1.1.11 // indirect
|
||||
golang.org/x/crypto v0.8.0 // indirect
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/crypto v0.7.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/net v0.9.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
245
go.sum
245
go.sum
@@ -42,19 +42,37 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||
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/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
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/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
|
||||
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
@@ -70,6 +88,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
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/deepmap/oapi-codegen v1.12.5-0.20230513000919-14548c7e7bbe h1:lgPRUK8nlnjJeXJ63G3zKxk0n1SLr90cLGWerslmFQE=
|
||||
github.com/deepmap/oapi-codegen v1.12.5-0.20230513000919-14548c7e7bbe/go.mod h1:rey/E8Zmlg0o3jo02vrDZMSv6YeWY/I8j3FTeR+78EU=
|
||||
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=
|
||||
@@ -99,25 +119,55 @@ 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/getkin/kin-openapi v0.116.0 h1:o986hwgMzR972JzOG5j6+WTwWqllZLs1EJKMKCivs2E=
|
||||
github.com/getkin/kin-openapi v0.116.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
|
||||
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
|
||||
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/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/httprate v0.7.1 h1:d5kXARdms2PREQfU4pHvq44S6hJ1hPu4OXLeBKmCKWs=
|
||||
github.com/go-chi/httprate v0.7.1/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-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-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU=
|
||||
github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
|
||||
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
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=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -144,6 +194,7 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
@@ -160,6 +211,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/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=
|
||||
@@ -189,6 +241,8 @@ 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/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
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=
|
||||
@@ -204,16 +258,31 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
|
||||
github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
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=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60=
|
||||
github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
|
||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -221,10 +290,16 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
|
||||
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
|
||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
||||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||
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/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
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/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
@@ -238,25 +313,48 @@ github.com/lestrrat-go/jwx/v2 v2.0.9/go.mod h1:K68euYaR95FnL0hIQB8VvzL70vB7pSifb
|
||||
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/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
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-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
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-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/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY=
|
||||
github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4=
|
||||
github.com/mileusna/useragent v1.3.2 h1:yGBQVNkyrlnSe4l0rlaQoH8XlG9xDkc6a7ygwPxALoU=
|
||||
github.com/mileusna/useragent v1.3.2/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
|
||||
github.com/mileusna/useragent v1.2.1 h1:p3RJWhi3LfuI6BHdddojREyK3p6qX67vIfOVMnUIVr0=
|
||||
github.com/mileusna/useragent v1.2.1/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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
@@ -264,29 +362,50 @@ 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.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE=
|
||||
github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM=
|
||||
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
|
||||
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
|
||||
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.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
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/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw=
|
||||
github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/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/pressly/goose/v3 v3.10.0 h1:Gn5E9CkPqTtWvfaDVqtJqMjYtsrZ9K5mU/8wzTsvg04=
|
||||
github.com/pressly/goose/v3 v3.10.0/go.mod h1:c5D3a7j66cT0fhRPj7KsXolfduVrhLlxKZjmCVSey5w=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
|
||||
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/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/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
|
||||
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
@@ -298,7 +417,9 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
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.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
@@ -317,12 +438,14 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
|
||||
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -334,8 +457,19 @@ github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
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/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
|
||||
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
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/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
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=
|
||||
@@ -353,6 +487,9 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@@ -361,9 +498,8 @@ 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 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
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/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=
|
||||
@@ -410,6 +546,7 @@ golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -417,6 +554,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -441,11 +579,14 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
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.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
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=
|
||||
@@ -455,6 +596,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -467,11 +610,12 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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=
|
||||
@@ -487,6 +631,7 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -499,28 +644,38 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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-20210603081109-ebe580a85c40/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/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-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-20220811171246-fbc7d0a398ab/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.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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=
|
||||
@@ -531,6 +686,7 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
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.6/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=
|
||||
@@ -594,8 +750,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.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
|
||||
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
|
||||
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=
|
||||
@@ -690,10 +846,13 @@ 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.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
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-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
@@ -701,10 +860,17 @@ 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/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.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@@ -714,16 +880,17 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
|
||||
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/sqlite v1.22.1 h1:P2+Dhp5FR1RlVRkQ3dDfCiv3Ok8XPxqpe70IjYVA9oE=
|
||||
modernc.org/sqlite v1.21.0 h1:4aP4MdUf15i3R3M2mx6Q90WHKz3nZLoz96zlB6tNdow=
|
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
||||
@@ -20,6 +20,12 @@ type Album struct {
|
||||
AllArtistIDs string `structs:"all_artist_ids" json:"allArtistIds" orm:"column(all_artist_ids)"`
|
||||
MaxYear int `structs:"max_year" json:"maxYear"`
|
||||
MinYear int `structs:"min_year" json:"minYear"`
|
||||
Date string `structs:"date" json:"date,omitempty"`
|
||||
MaxOriginalYear int `structs:"max_original_year" json:"maxOriginalYear"`
|
||||
MinOriginalYear int `structs:"min_original_year" json:"minOriginalYear"`
|
||||
OriginalDate string `structs:"original_date" json:"originalDate,omitempty"`
|
||||
ReleaseDate string `structs:"release_date" json:"releaseDate,omitempty"`
|
||||
Releases int `structs:"releases" json:"releases"`
|
||||
Compilation bool `structs:"compilation" json:"compilation"`
|
||||
Comment string `structs:"comment" json:"comment,omitempty"`
|
||||
SongCount int `structs:"song_count" json:"songCount"`
|
||||
@@ -54,9 +60,14 @@ func (a Album) CoverArtID() ArtworkID {
|
||||
return artworkIDFromAlbum(a)
|
||||
}
|
||||
|
||||
func (a Album) ArtistIDs() []string {
|
||||
return []string{a.ArtistID, a.AlbumArtistID}
|
||||
}
|
||||
|
||||
type DiscID struct {
|
||||
AlbumID string `json:"albumId"`
|
||||
DiscNumber int `json:"discNumber"`
|
||||
AlbumID string `json:"albumId"`
|
||||
ReleaseDate string `json:"releaseDate"`
|
||||
DiscNumber int `json:"discNumber"`
|
||||
}
|
||||
|
||||
type Albums []Album
|
||||
@@ -84,6 +95,14 @@ func (als Albums) ToAlbumArtist() Artist {
|
||||
return a
|
||||
}
|
||||
|
||||
func (als Albums) ArtistIDs() []string {
|
||||
var ids []string
|
||||
for _, al := range als {
|
||||
ids = append(ids, al.ArtistIDs()...)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
type AlbumRepository interface {
|
||||
CountAll(...QueryOptions) (int64, error)
|
||||
Exists(id string) (bool, error)
|
||||
|
||||
@@ -15,6 +15,11 @@ var fieldMap = map[string]*mappedField{
|
||||
"tracknumber": {field: "media_file.track_number"},
|
||||
"discnumber": {field: "media_file.disc_number"},
|
||||
"year": {field: "media_file.year"},
|
||||
"date": {field: "media_file.date"},
|
||||
"originalyear": {field: "media_file.original_year"},
|
||||
"originaldate": {field: "media_file.original_date"},
|
||||
"releaseyear": {field: "media_file.release_year"},
|
||||
"releasedate": {field: "media_file.release_date"},
|
||||
"size": {field: "media_file.size"},
|
||||
"compilation": {field: "media_file.compilation"},
|
||||
"dateadded": {field: "media_file.created_at"},
|
||||
|
||||
@@ -3,6 +3,7 @@ package model
|
||||
import (
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -32,6 +33,11 @@ type MediaFile struct {
|
||||
DiscNumber int `structs:"disc_number" json:"discNumber"`
|
||||
DiscSubtitle string `structs:"disc_subtitle" json:"discSubtitle,omitempty"`
|
||||
Year int `structs:"year" json:"year"`
|
||||
Date string `structs:"date" json:"date,omitempty"`
|
||||
OriginalYear int `structs:"original_year" json:"originalYear"`
|
||||
OriginalDate string `structs:"original_date" json:"originalDate,omitempty"`
|
||||
ReleaseYear int `structs:"release_year" json:"releaseYear"`
|
||||
ReleaseDate string `structs:"release_date" json:"releaseDate,omitempty"`
|
||||
Size int64 `structs:"size" json:"size"`
|
||||
Suffix string `structs:"suffix" json:"suffix"`
|
||||
Duration float32 `structs:"duration" json:"duration"`
|
||||
@@ -108,6 +114,11 @@ func (mfs MediaFiles) ToAlbum() Album {
|
||||
var songArtistIds []string
|
||||
var mbzAlbumIds []string
|
||||
var comments []string
|
||||
var years []int
|
||||
var dates []string
|
||||
var originalYears []int
|
||||
var originalDates []string
|
||||
var releaseDates []string
|
||||
for _, m := range mfs {
|
||||
// We assume these attributes are all the same for all songs on an album
|
||||
a.ID = m.AlbumID
|
||||
@@ -130,12 +141,11 @@ func (mfs MediaFiles) ToAlbum() Album {
|
||||
// Calculated attributes based on aggregations
|
||||
a.Duration += m.Duration
|
||||
a.Size += m.Size
|
||||
if a.MinYear == 0 {
|
||||
a.MinYear = m.Year
|
||||
} else if m.Year > 0 {
|
||||
a.MinYear = number.Min(a.MinYear, m.Year)
|
||||
}
|
||||
a.MaxYear = number.Max(a.MaxYear, m.Year)
|
||||
years = append(years, m.Year)
|
||||
dates = append(dates, m.Date)
|
||||
originalYears = append(originalYears, m.OriginalYear)
|
||||
originalDates = append(originalDates, m.OriginalDate)
|
||||
releaseDates = append(releaseDates, m.ReleaseDate)
|
||||
a.UpdatedAt = newer(a.UpdatedAt, m.UpdatedAt)
|
||||
a.CreatedAt = older(a.CreatedAt, m.CreatedAt)
|
||||
a.Genres = append(a.Genres, m.Genres...)
|
||||
@@ -151,11 +161,15 @@ func (mfs MediaFiles) ToAlbum() Album {
|
||||
a.EmbedArtPath = m.Path
|
||||
}
|
||||
}
|
||||
|
||||
a.Paths = strings.Join(mfs.Dirs(), consts.Zwsp)
|
||||
comments = slices.Compact(comments)
|
||||
if len(comments) == 1 {
|
||||
a.Comment = comments[0]
|
||||
}
|
||||
a.Date, _ = allOrNothing(dates)
|
||||
a.OriginalDate, _ = allOrNothing(originalDates)
|
||||
a.ReleaseDate, a.Releases = allOrNothing(releaseDates)
|
||||
a.MinYear, a.MaxYear = minMax(years)
|
||||
a.MinOriginalYear, a.MaxOriginalYear = minMax(originalYears)
|
||||
a.Comment, _ = allOrNothing(comments)
|
||||
a.Comment, _ = allOrNothing(comments)
|
||||
a.Genre = slice.MostFrequent(a.Genres).Name
|
||||
slices.SortFunc(a.Genres, func(a, b Genre) bool { return a.ID < b.ID })
|
||||
a.Genres = slices.Compact(a.Genres)
|
||||
@@ -169,6 +183,48 @@ func (mfs MediaFiles) ToAlbum() Album {
|
||||
return a
|
||||
}
|
||||
|
||||
func (mfs MediaFiles) ArtistIDs() []string {
|
||||
var ids []string
|
||||
for _, mf := range mfs {
|
||||
ids = append(ids, mf.ArtistID, mf.AlbumArtistID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (mfs MediaFiles) AlbumIDs() []string {
|
||||
var ids []string
|
||||
for _, mf := range mfs {
|
||||
ids = append(ids, mf.AlbumID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func allOrNothing(items []string) (string, int) {
|
||||
items = slices.Compact(items)
|
||||
if len(items) == 1 {
|
||||
return items[0], 1
|
||||
}
|
||||
if len(items) > 1 {
|
||||
sort.Strings(items)
|
||||
return "", len(slices.Compact(items))
|
||||
}
|
||||
return "", 0
|
||||
}
|
||||
|
||||
func minMax(items []int) (int, int) {
|
||||
var max = items[0]
|
||||
var min = items[0]
|
||||
for _, value := range items {
|
||||
max = number.Max(max, value)
|
||||
if min == 0 {
|
||||
min = value
|
||||
} else if value > 0 {
|
||||
min = number.Min(min, value)
|
||||
}
|
||||
}
|
||||
return min, max
|
||||
}
|
||||
|
||||
func newer(t1, t2 time.Time) time.Time {
|
||||
if t1.After(t2) {
|
||||
return t1
|
||||
|
||||
@@ -60,7 +60,7 @@ var _ = Describe("MediaFiles", func() {
|
||||
When("we have only one song", func() {
|
||||
BeforeEach(func() {
|
||||
mfs = MediaFiles{
|
||||
{Duration: 100.2, Size: 1024, Year: 1985, UpdatedAt: t("2022-12-19 09:30"), CreatedAt: t("2022-12-19 08:30")},
|
||||
{Duration: 100.2, Size: 1024, Year: 1985, Date: "1985-01-02", UpdatedAt: t("2022-12-19 09:30"), CreatedAt: t("2022-12-19 08:30")},
|
||||
}
|
||||
})
|
||||
It("calculates the aggregates correctly", func() {
|
||||
@@ -69,17 +69,18 @@ var _ = Describe("MediaFiles", func() {
|
||||
Expect(album.Size).To(Equal(int64(1024)))
|
||||
Expect(album.MinYear).To(Equal(1985))
|
||||
Expect(album.MaxYear).To(Equal(1985))
|
||||
Expect(album.Date).To(Equal("1985-01-02"))
|
||||
Expect(album.UpdatedAt).To(Equal(t("2022-12-19 09:30")))
|
||||
Expect(album.CreatedAt).To(Equal(t("2022-12-19 08:30")))
|
||||
})
|
||||
})
|
||||
|
||||
When("we have multiple songs", func() {
|
||||
When("we have multiple songs with different dates", func() {
|
||||
BeforeEach(func() {
|
||||
mfs = MediaFiles{
|
||||
{Duration: 100.2, Size: 1024, Year: 1985, UpdatedAt: t("2022-12-19 09:30"), CreatedAt: t("2022-12-19 08:30")},
|
||||
{Duration: 200.2, Size: 2048, Year: 0, UpdatedAt: t("2022-12-19 09:45"), CreatedAt: t("2022-12-19 08:30")},
|
||||
{Duration: 150.6, Size: 1000, Year: 1986, UpdatedAt: t("2022-12-19 09:45"), CreatedAt: t("2022-12-19 07:30")},
|
||||
{Duration: 100.2, Size: 1024, Year: 1985, Date: "1985-01-02", UpdatedAt: t("2022-12-19 09:30"), CreatedAt: t("2022-12-19 08:30")},
|
||||
{Duration: 200.2, Size: 2048, Year: 0, Date: "", UpdatedAt: t("2022-12-19 09:45"), CreatedAt: t("2022-12-19 08:30")},
|
||||
{Duration: 150.6, Size: 1000, Year: 1986, Date: "1986-01-02", UpdatedAt: t("2022-12-19 09:45"), CreatedAt: t("2022-12-19 07:30")},
|
||||
}
|
||||
})
|
||||
It("calculates the aggregates correctly", func() {
|
||||
@@ -88,6 +89,7 @@ var _ = Describe("MediaFiles", func() {
|
||||
Expect(album.Size).To(Equal(int64(4072)))
|
||||
Expect(album.MinYear).To(Equal(1985))
|
||||
Expect(album.MaxYear).To(Equal(1986))
|
||||
Expect(album.Date).To(BeEmpty())
|
||||
Expect(album.UpdatedAt).To(Equal(t("2022-12-19 09:45")))
|
||||
Expect(album.CreatedAt).To(Equal(t("2022-12-19 07:30")))
|
||||
})
|
||||
@@ -104,6 +106,21 @@ var _ = Describe("MediaFiles", func() {
|
||||
})
|
||||
})
|
||||
})
|
||||
When("we have multiple songs with same dates", func() {
|
||||
BeforeEach(func() {
|
||||
mfs = MediaFiles{
|
||||
{Duration: 100.2, Size: 1024, Year: 1985, Date: "1985-01-02", UpdatedAt: t("2022-12-19 09:30"), CreatedAt: t("2022-12-19 08:30")},
|
||||
{Duration: 200.2, Size: 2048, Year: 1985, Date: "1985-01-02", UpdatedAt: t("2022-12-19 09:45"), CreatedAt: t("2022-12-19 08:30")},
|
||||
{Duration: 150.6, Size: 1000, Year: 1985, Date: "1985-01-02", UpdatedAt: t("2022-12-19 09:45"), CreatedAt: t("2022-12-19 07:30")},
|
||||
}
|
||||
})
|
||||
It("sets the date field correctly", func() {
|
||||
album := mfs.ToAlbum()
|
||||
Expect(album.Date).To(Equal("1985-01-02"))
|
||||
Expect(album.MinYear).To(Equal(1985))
|
||||
Expect(album.MaxYear).To(Equal(1985))
|
||||
})
|
||||
})
|
||||
})
|
||||
Context("Calculated attributes", func() {
|
||||
Context("Genres", func() {
|
||||
|
||||
@@ -27,7 +27,7 @@ func NewAlbumRepository(ctx context.Context, o orm.QueryExecutor) model.AlbumRep
|
||||
"name": "order_album_name asc, order_album_artist_name asc",
|
||||
"artist": "compilation asc, order_album_artist_name asc, order_album_name asc",
|
||||
"random": "RANDOM()",
|
||||
"max_year": "max_year asc, name, order_album_name asc",
|
||||
"max_year": "coalesce(nullif(original_date,''), cast(max_year as text)), release_date, name, order_album_name asc",
|
||||
"recently_added": recentlyAddedSort(),
|
||||
}
|
||||
r.filterMappings = map[string]filterFunc{
|
||||
|
||||
@@ -26,8 +26,8 @@ func NewMediaFileRepository(ctx context.Context, o orm.QueryExecutor) *mediaFile
|
||||
r.ormer = o
|
||||
r.tableName = "media_file"
|
||||
r.sortMappings = map[string]string{
|
||||
"artist": "order_artist_name asc, order_album_name asc, disc_number asc, track_number asc",
|
||||
"album": "order_album_name asc, disc_number asc, track_number asc, order_artist_name asc, title asc",
|
||||
"artist": "order_artist_name asc, order_album_name asc, release_date asc, disc_number asc, track_number asc",
|
||||
"album": "order_album_name asc, release_date asc, disc_number asc, track_number asc, order_artist_name asc, title asc",
|
||||
"random": "RANDOM()",
|
||||
}
|
||||
r.filterMappings = map[string]filterFunc{
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"github.com/deluan/rest"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/slice"
|
||||
)
|
||||
|
||||
type playlistTrackRepository struct {
|
||||
@@ -123,7 +123,7 @@ func (r *playlistTrackRepository) Add(mediaFileIds []string) (int, error) {
|
||||
}
|
||||
|
||||
func (r *playlistTrackRepository) addMediaFileIds(cond Sqlizer) (int, error) {
|
||||
sq := Select("id").From("media_file").Where(cond).OrderBy("album_artist, album, disc_number, track_number")
|
||||
sq := Select("id").From("media_file").Where(cond).OrderBy("album_artist, album, release_date, disc_number, track_number")
|
||||
var ids []string
|
||||
err := r.queryAll(sq, &ids)
|
||||
if err != nil {
|
||||
@@ -147,7 +147,7 @@ func (r *playlistTrackRepository) AddDiscs(discs []model.DiscID) (int, error) {
|
||||
}
|
||||
var clauses Or
|
||||
for _, d := range discs {
|
||||
clauses = append(clauses, And{Eq{"album_id": d.AlbumID}, Eq{"disc_number": d.DiscNumber}})
|
||||
clauses = append(clauses, And{Eq{"album_id": d.AlbumID}, Eq{"release_date": d.ReleaseDate}, Eq{"disc_number": d.DiscNumber}})
|
||||
}
|
||||
return r.addMediaFileIds(clauses)
|
||||
}
|
||||
@@ -196,7 +196,7 @@ func (r *playlistTrackRepository) Reorder(pos int, newPos int) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newOrder := utils.MoveString(ids, pos-1, newPos-1)
|
||||
newOrder := slice.Move(ids, pos-1, newPos-1)
|
||||
return r.playlistRepo.updatePlaylist(r.playlistId, newOrder)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
)
|
||||
|
||||
type sqlRepository struct {
|
||||
@@ -78,9 +79,9 @@ func (r sqlRepository) buildSortOrder(sort, order string) string {
|
||||
}
|
||||
|
||||
var newSort []string
|
||||
parts := strings.FieldsFunc(sort, splitFunc(','))
|
||||
parts := strings.FieldsFunc(sort, utils.SplitFunc(','))
|
||||
for _, p := range parts {
|
||||
f := strings.FieldsFunc(p, splitFunc(' '))
|
||||
f := strings.FieldsFunc(p, utils.SplitFunc(' '))
|
||||
newField := []string{f[0]}
|
||||
if len(f) == 1 {
|
||||
newField = append(newField, order)
|
||||
@@ -96,21 +97,6 @@ func (r sqlRepository) buildSortOrder(sort, order string) string {
|
||||
return strings.Join(newSort, ", ")
|
||||
}
|
||||
|
||||
func splitFunc(delimiter rune) func(c rune) bool {
|
||||
open := false
|
||||
return func(c rune) bool {
|
||||
if open {
|
||||
open = c != ')'
|
||||
return false
|
||||
}
|
||||
if c == '(' {
|
||||
open = true
|
||||
return false
|
||||
}
|
||||
return c == delimiter
|
||||
}
|
||||
}
|
||||
|
||||
func (r sqlRepository) applyFilters(sq SelectBuilder, options ...model.QueryOptions) SelectBuilder {
|
||||
if len(options) > 0 && options[0].Filters != nil {
|
||||
sq = sq.Where(options[0].Filters)
|
||||
|
||||
@@ -69,6 +69,10 @@ var _ = Describe("sqlRepository", func() {
|
||||
sql := r.buildSortOrder("name desc, substr(id, 7), status asc", "desc")
|
||||
Expect(sql).To(Equal("name asc, substr(id, 7) desc, status desc"))
|
||||
})
|
||||
It("handles nested functions", func() {
|
||||
sql := r.buildSortOrder("name desc, coalesce(nullif(release_date, ''), nullif(original_date, '')), status asc", "desc")
|
||||
Expect(sql).To(Equal("name asc, coalesce(nullif(release_date, ''), nullif(original_date, '')) desc, status desc"))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,9 +32,10 @@ func newMediaFileMapper(rootFolder string, genres model.GenreRepository) *mediaF
|
||||
func (s mediaFileMapper) toMediaFile(md metadata.Tags) model.MediaFile {
|
||||
mf := &model.MediaFile{}
|
||||
mf.ID = s.trackID(md)
|
||||
mf.Year, mf.Date, mf.OriginalYear, mf.OriginalDate, mf.ReleaseYear, mf.ReleaseDate = s.mapDates(md)
|
||||
mf.Title = s.mapTrackTitle(md)
|
||||
mf.Album = md.Album()
|
||||
mf.AlbumID = s.albumID(md)
|
||||
mf.AlbumID = s.albumID(md, mf.ReleaseDate)
|
||||
mf.Album = s.mapAlbumName(md)
|
||||
mf.ArtistID = s.artistID(md)
|
||||
mf.Artist = s.mapArtistName(md)
|
||||
@@ -42,7 +43,6 @@ func (s mediaFileMapper) toMediaFile(md metadata.Tags) model.MediaFile {
|
||||
mf.AlbumArtist = s.mapAlbumArtistName(md)
|
||||
mf.Genre, mf.Genres = s.mapGenres(md.Genres())
|
||||
mf.Compilation = md.Compilation()
|
||||
mf.Year = md.Year()
|
||||
mf.TrackNumber, _ = md.TrackNumber()
|
||||
mf.DiscNumber, _ = md.DiscNumber()
|
||||
mf.DiscSubtitle = md.DiscSubtitle()
|
||||
@@ -128,8 +128,13 @@ func (s mediaFileMapper) trackID(md metadata.Tags) string {
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(md.FilePath())))
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) albumID(md metadata.Tags) string {
|
||||
func (s mediaFileMapper) albumID(md metadata.Tags, releaseDate string) string {
|
||||
albumPath := strings.ToLower(fmt.Sprintf("%s\\%s", s.mapAlbumArtistName(md), s.mapAlbumName(md)))
|
||||
if !conf.Server.Scanner.GroupAlbumReleases {
|
||||
if len(releaseDate) != 0 {
|
||||
albumPath = fmt.Sprintf("%s\\%s", albumPath, releaseDate)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(albumPath)))
|
||||
}
|
||||
|
||||
@@ -169,3 +174,18 @@ func (s mediaFileMapper) mapGenres(genres []string) (string, model.Genres) {
|
||||
}
|
||||
return result[0].Name, result
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) mapDates(md metadata.Tags) (int, string, int, string, int, string) {
|
||||
year, date := md.Date()
|
||||
originalYear, originalDate := md.OriginalDate()
|
||||
releaseYear, releaseDate := md.ReleaseDate()
|
||||
|
||||
// MusicBrainz Picard writes the Release Date of an album to the Date tag, and leaves the Release Date tag empty
|
||||
taggedLikePicard := (originalYear != 0) &&
|
||||
(releaseYear == 0) &&
|
||||
(year >= originalYear)
|
||||
if taggedLikePicard {
|
||||
return originalYear, originalDate, originalYear, originalDate, year, date
|
||||
}
|
||||
return year, date, originalYear, originalDate, releaseYear, releaseDate
|
||||
}
|
||||
|
||||
@@ -43,8 +43,9 @@ func (e *Extractor) Parse(files ...string) (map[string]metadata.ParsedTags, erro
|
||||
|
||||
func (e *Extractor) CustomMappings() metadata.ParsedTags {
|
||||
return metadata.ParsedTags{
|
||||
"disc": {"tpa"},
|
||||
"has_picture": {"metadata_block_picture"},
|
||||
"disc": {"tpa"},
|
||||
"has_picture": {"metadata_block_picture"},
|
||||
"originaldate": {"tdor"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,17 +94,19 @@ func (t Tags) Artist() string { return t.getFirstTagValue("artist", "sort_artist
|
||||
func (t Tags) AlbumArtist() string {
|
||||
return t.getFirstTagValue("album_artist", "album artist", "albumartist")
|
||||
}
|
||||
func (t Tags) SortTitle() string { return t.getSortTag("", "title", "name") }
|
||||
func (t Tags) SortAlbum() string { return t.getSortTag("", "album") }
|
||||
func (t Tags) SortArtist() string { return t.getSortTag("", "artist") }
|
||||
func (t Tags) SortAlbumArtist() string { return t.getSortTag("tso2", "albumartist", "album_artist") }
|
||||
func (t Tags) Genres() []string { return t.getAllTagValues("genre") }
|
||||
func (t Tags) Year() int { return t.getYear("date") }
|
||||
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) SortTitle() string { return t.getSortTag("", "title", "name") }
|
||||
func (t Tags) SortAlbum() string { return t.getSortTag("", "album") }
|
||||
func (t Tags) SortArtist() string { return t.getSortTag("", "artist") }
|
||||
func (t Tags) SortAlbumArtist() string { return t.getSortTag("tso2", "albumartist", "album_artist") }
|
||||
func (t Tags) Genres() []string { return t.getAllTagValues("genre") }
|
||||
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) DiscSubtitle() string {
|
||||
return t.getFirstTagValue("tsst", "discsubtitle", "setsubtitle")
|
||||
}
|
||||
@@ -217,18 +219,38 @@ func (t Tags) getSortTag(originalTag string, tagNames ...string) string {
|
||||
|
||||
var dateRegex = regexp.MustCompile(`([12]\d\d\d)`)
|
||||
|
||||
func (t Tags) getYear(tagNames ...string) int {
|
||||
func (t Tags) getDate(tagNames ...string) (int, string) {
|
||||
tag := t.getFirstTagValue(tagNames...)
|
||||
if tag == "" {
|
||||
return 0
|
||||
if len(tag) < 4 {
|
||||
return 0, ""
|
||||
}
|
||||
// first get just the year
|
||||
match := dateRegex.FindStringSubmatch(tag)
|
||||
if len(match) == 0 {
|
||||
log.Warn("Error parsing year date field", "file", t.filePath, "date", tag)
|
||||
return 0
|
||||
log.Warn("Error parsing "+tagNames[0]+" field for year", "file", t.filePath, "date", tag)
|
||||
return 0, ""
|
||||
}
|
||||
year, _ := strconv.Atoi(match[1])
|
||||
return year
|
||||
|
||||
if len(tag) < 5 {
|
||||
return year, match[1]
|
||||
}
|
||||
|
||||
//then try YYYY-MM-DD
|
||||
if len(tag) > 10 {
|
||||
tag = tag[:10]
|
||||
}
|
||||
layout := "2006-01-02"
|
||||
_, err := time.Parse(layout, tag)
|
||||
if err != nil {
|
||||
layout = "2006-01"
|
||||
_, err = time.Parse(layout, tag)
|
||||
if err != nil {
|
||||
log.Warn("Error parsing "+tagNames[0]+" field for month + day", "file", t.filePath, "date", tag)
|
||||
return year, match[1]
|
||||
}
|
||||
}
|
||||
return year, tag
|
||||
}
|
||||
|
||||
func (t Tags) getBool(tagNames ...string) bool {
|
||||
|
||||
@@ -6,31 +6,25 @@ import (
|
||||
)
|
||||
|
||||
var _ = Describe("Tags", func() {
|
||||
Describe("getYear", func() {
|
||||
It("parses the year correctly", func() {
|
||||
var examples = map[string]int{
|
||||
"1985": 1985,
|
||||
"2002-01": 2002,
|
||||
"1969.06": 1969,
|
||||
"1980.07.25": 1980,
|
||||
"2004-00-00": 2004,
|
||||
"2013-May-12": 2013,
|
||||
"May 12, 2016": 2016,
|
||||
"01/10/1990": 1990,
|
||||
}
|
||||
for tag, expected := range examples {
|
||||
md := &Tags{}
|
||||
md.tags = map[string][]string{"date": {tag}}
|
||||
Expect(md.Year()).To(Equal(expected))
|
||||
}
|
||||
})
|
||||
|
||||
It("returns 0 if year is invalid", func() {
|
||||
DescribeTable("getDate",
|
||||
func(tag string, expectedYear int, expectedDate string) {
|
||||
md := &Tags{}
|
||||
md.tags = map[string][]string{"date": {"invalid"}}
|
||||
Expect(md.Year()).To(Equal(0))
|
||||
})
|
||||
})
|
||||
md.tags = map[string][]string{"date": {tag}}
|
||||
testYear, testDate := md.Date()
|
||||
Expect(testYear).To(Equal(expectedYear))
|
||||
Expect(testDate).To(Equal(expectedDate))
|
||||
},
|
||||
Entry(nil, "1985", 1985, "1985"),
|
||||
Entry(nil, "2002-01", 2002, "2002-01"),
|
||||
Entry(nil, "1969.06", 1969, "1969"),
|
||||
Entry(nil, "1980.07.25", 1980, "1980"),
|
||||
Entry(nil, "2004-00-00", 2004, "2004"),
|
||||
Entry(nil, "2016-12-31", 2016, "2016-12-31"),
|
||||
Entry(nil, "2013-May-12", 2013, "2013"),
|
||||
Entry(nil, "May 12, 2016", 2016, "2016"),
|
||||
Entry(nil, "01/10/1990", 1990, "1990"),
|
||||
Entry(nil, "invalid", 0, ""),
|
||||
)
|
||||
|
||||
Describe("getMbzID", func() {
|
||||
It("return a valid MBID", func() {
|
||||
|
||||
@@ -27,7 +27,15 @@ var _ = Describe("Tags", func() {
|
||||
Expect(m.AlbumArtist()).To(Equal("Album Artist"))
|
||||
Expect(m.Compilation()).To(BeTrue())
|
||||
Expect(m.Genres()).To(Equal([]string{"Rock"}))
|
||||
Expect(m.Year()).To(Equal(2014))
|
||||
y, d := m.Date()
|
||||
Expect(y).To(Equal(2014))
|
||||
Expect(d).To(Equal("2014-05-21"))
|
||||
y, d = m.OriginalDate()
|
||||
Expect(y).To(Equal(1996))
|
||||
Expect(d).To(Equal("1996-11-21"))
|
||||
y, d = m.ReleaseDate()
|
||||
Expect(y).To(Equal(2020))
|
||||
Expect(d).To(Equal("2020-12-31"))
|
||||
n, t := m.TrackNumber()
|
||||
Expect(n).To(Equal(2))
|
||||
Expect(t).To(Equal(10))
|
||||
@@ -53,10 +61,10 @@ var _ = Describe("Tags", func() {
|
||||
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(5065)))
|
||||
Expect(m.Size()).To(Equal(int64(5178)))
|
||||
// TabLib 1.12 returns 18, previous versions return 39.
|
||||
// See https://github.com/taglib/taglib/commit/2f238921824741b2cfe6fbfbfc9701d9827ab06b
|
||||
Expect(m.BitRate()).To(BeElementOf(18, 39))
|
||||
Expect(m.BitRate()).To(BeElementOf(18, 39, 40))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -41,7 +41,9 @@ var _ = Describe("Extractor", func() {
|
||||
Expect(m).To(HaveKeyWithValue("albumartist", []string{"Album Artist"}))
|
||||
Expect(m).To(HaveKeyWithValue("tcmp", []string{"1"})) // Compilation
|
||||
Expect(m).To(HaveKeyWithValue("genre", []string{"Rock"}))
|
||||
Expect(m).To(HaveKeyWithValue("date", []string{"2014", "2014"}))
|
||||
Expect(m).To(HaveKeyWithValue("date", []string{"2014-05-21", "2014"}))
|
||||
Expect(m).To(HaveKeyWithValue("originaldate", []string{"1996-11-21"}))
|
||||
Expect(m).To(HaveKeyWithValue("releasedate", []string{"2020-12-31"}))
|
||||
Expect(m).To(HaveKeyWithValue("discnumber", []string{"1/2"}))
|
||||
Expect(m).To(HaveKeyWithValue("has_picture", []string{"true"}))
|
||||
Expect(m).To(HaveKeyWithValue("duration", []string{"1.02"}))
|
||||
@@ -65,8 +67,27 @@ 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"))
|
||||
Expect(m["bitrate"][0]).To(BeElementOf("18", "39", "40"))
|
||||
})
|
||||
|
||||
DescribeTable("ReplayGain",
|
||||
func(file, albumGain, albumPeak, trackGain, trackPeak string) {
|
||||
file = "tests/fixtures/" + file
|
||||
mds, err := e.Parse(file)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(mds).To(HaveLen(1))
|
||||
|
||||
m := mds[file]
|
||||
|
||||
Expect(m).To(HaveKeyWithValue("replaygain_album_gain", []string{albumGain}))
|
||||
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}))
|
||||
},
|
||||
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"),
|
||||
)
|
||||
})
|
||||
|
||||
Context("Error Checking", func() {
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
|
||||
#include "taglib_wrapper.h"
|
||||
|
||||
// Tags necessary for M4a parsing
|
||||
const char *RG_TAGS[] = {
|
||||
"replaygain_album_gain",
|
||||
"replaygain_album_peak",
|
||||
"replaygain_track_gain",
|
||||
"replaygain_track_peak"};
|
||||
|
||||
char has_cover(const TagLib::FileRef f);
|
||||
|
||||
int taglib_read(const FILENAME_CHAR_T *filename, unsigned long id) {
|
||||
@@ -70,6 +77,29 @@ int taglib_read(const FILENAME_CHAR_T *filename, unsigned long id) {
|
||||
}
|
||||
}
|
||||
|
||||
TagLib::MP4::File *m4afile(dynamic_cast<TagLib::MP4::File *>(f.file()));
|
||||
if (m4afile != NULL)
|
||||
{
|
||||
const auto itemListMap = m4afile->tag();
|
||||
{
|
||||
char buf[200];
|
||||
|
||||
for (const char *key : RG_TAGS)
|
||||
{
|
||||
snprintf(buf, sizeof(buf), "----:com.apple.iTunes:%s", key);
|
||||
const auto item = itemListMap->item(buf);
|
||||
if (item.isValid())
|
||||
{
|
||||
char *dup = ::strdup(key);
|
||||
char *val = ::strdup(item.toStringList().front().toCString(true));
|
||||
go_map_put_str(id, dup, val);
|
||||
free(dup);
|
||||
free(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (has_cover(f)) {
|
||||
go_map_put_str(id, (char *)"has_picture", (char *)"true");
|
||||
}
|
||||
|
||||
@@ -80,9 +80,10 @@ func (s *TagScanner) Scan(ctx context.Context, lastModifiedSince time.Time, prog
|
||||
|
||||
// Special case: if lastModifiedSince is zero, re-import all files
|
||||
fullScan := lastModifiedSince.IsZero()
|
||||
rootFS := os.DirFS(s.rootFolder)
|
||||
|
||||
// If the media folder is empty (no music and no subfolders), abort to avoid deleting all data from DB
|
||||
empty, err := isDirEmpty(ctx, s.rootFolder)
|
||||
empty, err := isDirEmpty(ctx, rootFS, ".")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -103,7 +104,9 @@ func (s *TagScanner) Scan(ctx context.Context, lastModifiedSince time.Time, prog
|
||||
s.mapper = newMediaFileMapper(s.rootFolder, genres)
|
||||
refresher := newRefresher(s.ds, s.cacheWarmer, allFSDirs)
|
||||
|
||||
foldersFound, walkerError := s.getRootFolderWalker(ctx)
|
||||
log.Trace(ctx, "Loading directory tree from music folder", "folder", s.rootFolder)
|
||||
foldersFound, walkerError := walkDirTree(ctx, rootFS, s.rootFolder)
|
||||
|
||||
for {
|
||||
folderStats, more := <-foldersFound
|
||||
if !more {
|
||||
@@ -166,30 +169,14 @@ func (s *TagScanner) Scan(ctx context.Context, lastModifiedSince time.Time, prog
|
||||
return s.cnt.total(), err
|
||||
}
|
||||
|
||||
func isDirEmpty(ctx context.Context, dir string) (bool, error) {
|
||||
children, stats, err := loadDir(ctx, dir)
|
||||
func isDirEmpty(ctx context.Context, rootFS fs.FS, dir string) (bool, error) {
|
||||
children, stats, err := loadDir(ctx, rootFS, dir)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return len(children) == 0 && stats.AudioFilesCount == 0, nil
|
||||
}
|
||||
|
||||
func (s *TagScanner) getRootFolderWalker(ctx context.Context) (walkResults, chan error) {
|
||||
start := time.Now()
|
||||
log.Trace(ctx, "Loading directory tree from music folder", "folder", s.rootFolder)
|
||||
results := make(chan dirStats, 5000)
|
||||
walkerError := make(chan error)
|
||||
go func() {
|
||||
err := walkDirTree(ctx, s.rootFolder, results)
|
||||
if err != nil {
|
||||
log.Error("There were errors reading directories from filesystem", err)
|
||||
}
|
||||
walkerError <- err
|
||||
log.Debug("Finished reading directories from filesystem", "elapsed", time.Since(start))
|
||||
}()
|
||||
return results, walkerError
|
||||
}
|
||||
|
||||
func (s *TagScanner) getDBDirTree(ctx context.Context) (map[string]struct{}, error) {
|
||||
start := time.Now()
|
||||
log.Trace(ctx, "Loading directory tree from database", "folder", s.rootFolder)
|
||||
|
||||
@@ -10,11 +10,12 @@ 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(4))
|
||||
Expect(files).To(HaveLen(5))
|
||||
Expect(files).To(HaveKey("tests/fixtures/test.ogg"))
|
||||
Expect(files).To(HaveKey("tests/fixtures/test.mp3"))
|
||||
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"))
|
||||
Expect(files).ToNot(HaveKey("tests/fixtures/._02 Invisible.mp3"))
|
||||
Expect(files).ToNot(HaveKey("tests/fixtures/playlist.m3u"))
|
||||
})
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -13,7 +12,6 @@ import (
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -25,31 +23,37 @@ type (
|
||||
HasPlaylist bool
|
||||
AudioFilesCount uint32
|
||||
}
|
||||
walkResults = chan dirStats
|
||||
)
|
||||
|
||||
func walkDirTree(ctx context.Context, rootFolder string, results walkResults) error {
|
||||
err := walkFolder(ctx, rootFolder, rootFolder, results)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error loading directory tree", err)
|
||||
}
|
||||
close(results)
|
||||
return err
|
||||
func walkDirTree(ctx context.Context, fsys fs.FS, rootFolder string) (<-chan dirStats, chan error) {
|
||||
results := make(chan dirStats)
|
||||
errC := make(chan error)
|
||||
go func() {
|
||||
defer close(results)
|
||||
defer close(errC)
|
||||
err := walkFolder(ctx, fsys, rootFolder, ".", results)
|
||||
if err != nil {
|
||||
log.Error(ctx, "There were errors reading directories from filesystem", "path", rootFolder, err)
|
||||
errC <- err
|
||||
}
|
||||
log.Debug(ctx, "Finished reading directories from filesystem", "path", rootFolder)
|
||||
}()
|
||||
return results, errC
|
||||
}
|
||||
|
||||
func walkFolder(ctx context.Context, rootPath string, currentFolder string, results walkResults) error {
|
||||
children, stats, err := loadDir(ctx, currentFolder)
|
||||
func walkFolder(ctx context.Context, fsys fs.FS, rootPath string, currentFolder string, results chan<- dirStats) error {
|
||||
children, stats, err := loadDir(ctx, fsys, currentFolder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range children {
|
||||
err := walkFolder(ctx, rootPath, c, results)
|
||||
err := walkFolder(ctx, fsys, rootPath, c, results)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
dir := filepath.Clean(currentFolder)
|
||||
dir := filepath.Clean(filepath.Join(rootPath, currentFolder))
|
||||
log.Trace(ctx, "Found directory", "dir", dir, "audioCount", stats.AudioFilesCount,
|
||||
"images", stats.Images, "hasPlaylist", stats.HasPlaylist)
|
||||
stats.Path = dir
|
||||
@@ -58,33 +62,37 @@ func walkFolder(ctx context.Context, rootPath string, currentFolder string, resu
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadDir(ctx context.Context, dirPath string) ([]string, *dirStats, error) {
|
||||
func loadDir(ctx context.Context, fsys fs.FS, dirPath string) ([]string, *dirStats, error) {
|
||||
var children []string
|
||||
stats := &dirStats{}
|
||||
|
||||
dirInfo, err := os.Stat(dirPath)
|
||||
dirInfo, err := fs.Stat(fsys, dirPath)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error stating dir", "path", dirPath, err)
|
||||
return nil, nil, err
|
||||
}
|
||||
stats.ModTime = dirInfo.ModTime()
|
||||
|
||||
dir, err := os.Open(dirPath)
|
||||
dir, err := fsys.Open(dirPath)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error in Opening directory", "path", dirPath, err)
|
||||
return children, stats, err
|
||||
}
|
||||
defer dir.Close()
|
||||
dirFile, ok := dir.(fs.ReadDirFile)
|
||||
if !ok {
|
||||
log.Error(ctx, "Not a directory", "path", dirPath)
|
||||
return children, stats, err
|
||||
}
|
||||
|
||||
dirEntries := fullReadDir(ctx, dir)
|
||||
for _, entry := range dirEntries {
|
||||
isDir, err := isDirOrSymlinkToDir(dirPath, entry)
|
||||
for _, entry := range fullReadDir(ctx, dirFile) {
|
||||
isDir, err := isDirOrSymlinkToDir(fsys, dirPath, entry)
|
||||
// Skip invalid symlinks
|
||||
if err != nil {
|
||||
log.Error(ctx, "Invalid symlink", "dir", filepath.Join(dirPath, entry.Name()), err)
|
||||
continue
|
||||
}
|
||||
if isDir && !isDirIgnored(dirPath, entry) && isDirReadable(dirPath, entry) {
|
||||
if isDir && !isDirIgnored(fsys, dirPath, entry) && isDirReadable(ctx, fsys, dirPath, entry) {
|
||||
children = append(children, filepath.Join(dirPath, entry.Name()))
|
||||
} else {
|
||||
fileInfo, err := entry.Info()
|
||||
@@ -113,14 +121,14 @@ func loadDir(ctx context.Context, dirPath string) ([]string, *dirStats, error) {
|
||||
|
||||
// fullReadDir reads all files in the folder, skipping the ones with errors.
|
||||
// It also detects when it is "stuck" with an error in the same directory over and over.
|
||||
// In this case, it and returns whatever it was able to read until it got stuck.
|
||||
// In this case, it stops and returns whatever it was able to read until it got stuck.
|
||||
// See discussion here: https://github.com/navidrome/navidrome/issues/1164#issuecomment-881922850
|
||||
func fullReadDir(ctx context.Context, dir fs.ReadDirFile) []os.DirEntry {
|
||||
var allDirs []os.DirEntry
|
||||
func fullReadDir(ctx context.Context, dir fs.ReadDirFile) []fs.DirEntry {
|
||||
var allEntries []fs.DirEntry
|
||||
var prevErrStr = ""
|
||||
for {
|
||||
dirs, err := dir.ReadDir(-1)
|
||||
allDirs = append(allDirs, dirs...)
|
||||
entries, err := dir.ReadDir(-1)
|
||||
allEntries = append(allEntries, entries...)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
@@ -131,8 +139,8 @@ func fullReadDir(ctx context.Context, dir fs.ReadDirFile) []os.DirEntry {
|
||||
}
|
||||
prevErrStr = err.Error()
|
||||
}
|
||||
sort.Slice(allDirs, func(i, j int) bool { return allDirs[i].Name() < allDirs[j].Name() })
|
||||
return allDirs
|
||||
sort.Slice(allEntries, func(i, j int) bool { return allEntries[i].Name() < allEntries[j].Name() })
|
||||
return allEntries
|
||||
}
|
||||
|
||||
// isDirOrSymlinkToDir returns true if and only if the dirEnt represents a file
|
||||
@@ -141,7 +149,7 @@ func fullReadDir(ctx context.Context, dir fs.ReadDirFile) []os.DirEntry {
|
||||
// sending a request to the operating system to follow the symbolic link.
|
||||
// originally copied from github.com/karrick/godirwalk, modified to use dirEntry for
|
||||
// efficiency for go 1.16 and beyond
|
||||
func isDirOrSymlinkToDir(baseDir string, dirEnt fs.DirEntry) (bool, error) {
|
||||
func isDirOrSymlinkToDir(fsys fs.FS, baseDir string, dirEnt fs.DirEntry) (bool, error) {
|
||||
if dirEnt.IsDir() {
|
||||
return true, nil
|
||||
}
|
||||
@@ -149,7 +157,7 @@ func isDirOrSymlinkToDir(baseDir string, dirEnt fs.DirEntry) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
// Does this symlink point to a directory?
|
||||
fileInfo, err := os.Stat(filepath.Join(baseDir, dirEnt.Name()))
|
||||
fileInfo, err := fs.Stat(fsys, filepath.Join(baseDir, dirEnt.Name()))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -157,26 +165,30 @@ func isDirOrSymlinkToDir(baseDir string, dirEnt fs.DirEntry) (bool, error) {
|
||||
}
|
||||
|
||||
// isDirIgnored returns true if the directory represented by dirEnt contains an
|
||||
// `ignore` file (named after consts.SkipScanFile)
|
||||
func isDirIgnored(baseDir string, dirEnt fs.DirEntry) bool {
|
||||
// allows Album folders for albums which e.g. start with ellipses
|
||||
name := dirEnt.Name()
|
||||
if strings.HasPrefix(name, ".") && !strings.HasPrefix(name, "..") {
|
||||
// `ignore` file (named after skipScanFile)
|
||||
func isDirIgnored(fsys fs.FS, baseDir string, dirEnt fs.DirEntry) bool {
|
||||
// allows Album folders for albums which eg start with ellipses
|
||||
if strings.HasPrefix(dirEnt.Name(), ".") && !strings.HasPrefix(dirEnt.Name(), "..") {
|
||||
return true
|
||||
}
|
||||
if runtime.GOOS == "windows" && strings.EqualFold(name, "$RECYCLE.BIN") {
|
||||
return true
|
||||
}
|
||||
_, err := os.Stat(filepath.Join(baseDir, name, consts.SkipScanFile))
|
||||
_, err := fs.Stat(fsys, filepath.Join(baseDir, dirEnt.Name(), consts.SkipScanFile))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// isDirReadable returns true if the directory represented by dirEnt is readable
|
||||
func isDirReadable(baseDir string, dirEnt fs.DirEntry) bool {
|
||||
func isDirReadable(ctx context.Context, fsys fs.FS, baseDir string, dirEnt fs.DirEntry) bool {
|
||||
path := filepath.Join(baseDir, dirEnt.Name())
|
||||
res, err := utils.IsDirReadable(path)
|
||||
if !res {
|
||||
|
||||
dir, err := fsys.Open(path)
|
||||
if err != nil {
|
||||
log.Warn("Skipping unreadable directory", "path", path, err)
|
||||
return false
|
||||
}
|
||||
return res
|
||||
|
||||
err = dir.Close()
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Error closing directory", "path", path, err)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package scanner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -13,16 +14,14 @@ import (
|
||||
)
|
||||
|
||||
var _ = Describe("walk_dir_tree", func() {
|
||||
baseDir := filepath.Join("tests", "fixtures")
|
||||
dir, _ := os.Getwd()
|
||||
baseDir := filepath.Join(dir, "tests", "fixtures")
|
||||
fsys := os.DirFS(baseDir)
|
||||
|
||||
Describe("walkDirTree", func() {
|
||||
It("reads all info correctly", func() {
|
||||
var collected = dirMap{}
|
||||
results := make(walkResults, 5000)
|
||||
var errC = make(chan error)
|
||||
go func() {
|
||||
errC <- walkDirTree(context.Background(), baseDir, results)
|
||||
}()
|
||||
results, errC := walkDirTree(context.Background(), fsys, baseDir)
|
||||
|
||||
for {
|
||||
stats, more := <-results
|
||||
@@ -32,11 +31,11 @@ var _ = Describe("walk_dir_tree", func() {
|
||||
collected[stats.Path] = stats
|
||||
}
|
||||
|
||||
Eventually(errC).Should(Receive(nil))
|
||||
Consistently(errC).ShouldNot(Receive())
|
||||
Expect(collected[baseDir]).To(MatchFields(IgnoreExtras, Fields{
|
||||
"Images": BeEmpty(),
|
||||
"HasPlaylist": BeFalse(),
|
||||
"AudioFilesCount": BeNumerically("==", 5),
|
||||
"AudioFilesCount": BeNumerically("==", 6),
|
||||
}))
|
||||
Expect(collected[filepath.Join(baseDir, "artist", "an-album")]).To(MatchFields(IgnoreExtras, Fields{
|
||||
"Images": ConsistOf("cover.jpg", "front.png", "artist.png"),
|
||||
@@ -51,42 +50,42 @@ var _ = Describe("walk_dir_tree", func() {
|
||||
|
||||
Describe("isDirOrSymlinkToDir", func() {
|
||||
It("returns true for normal dirs", func() {
|
||||
dirEntry, _ := getDirEntry("tests", "fixtures")
|
||||
Expect(isDirOrSymlinkToDir(baseDir, dirEntry)).To(BeTrue())
|
||||
dirEntry := getDirEntry("tests", "fixtures")
|
||||
Expect(isDirOrSymlinkToDir(fsys, ".", dirEntry)).To(BeTrue())
|
||||
})
|
||||
It("returns true for symlinks to dirs", func() {
|
||||
dirEntry, _ := getDirEntry(baseDir, "symlink2dir")
|
||||
Expect(isDirOrSymlinkToDir(baseDir, dirEntry)).To(BeTrue())
|
||||
dirEntry := getDirEntry(baseDir, "symlink2dir")
|
||||
Expect(isDirOrSymlinkToDir(fsys, ".", dirEntry)).To(BeTrue())
|
||||
})
|
||||
It("returns false for files", func() {
|
||||
dirEntry, _ := getDirEntry(baseDir, "test.mp3")
|
||||
Expect(isDirOrSymlinkToDir(baseDir, dirEntry)).To(BeFalse())
|
||||
dirEntry := getDirEntry(baseDir, "test.mp3")
|
||||
Expect(isDirOrSymlinkToDir(fsys, ".", dirEntry)).To(BeFalse())
|
||||
})
|
||||
It("returns false for symlinks to files", func() {
|
||||
dirEntry, _ := getDirEntry(baseDir, "symlink")
|
||||
Expect(isDirOrSymlinkToDir(baseDir, dirEntry)).To(BeFalse())
|
||||
dirEntry := getDirEntry(baseDir, "symlink")
|
||||
Expect(isDirOrSymlinkToDir(fsys, ".", dirEntry)).To(BeFalse())
|
||||
})
|
||||
})
|
||||
Describe("isDirIgnored", func() {
|
||||
It("returns false for normal dirs", func() {
|
||||
dirEntry, _ := getDirEntry(baseDir, "empty_folder")
|
||||
Expect(isDirIgnored(baseDir, dirEntry)).To(BeFalse())
|
||||
dirEntry := getDirEntry(baseDir, "empty_folder")
|
||||
Expect(isDirIgnored(fsys, ".", dirEntry)).To(BeFalse())
|
||||
})
|
||||
It("returns true when folder contains .ndignore file", func() {
|
||||
dirEntry, _ := getDirEntry(baseDir, "ignored_folder")
|
||||
Expect(isDirIgnored(baseDir, dirEntry)).To(BeTrue())
|
||||
dirEntry := getDirEntry(baseDir, "ignored_folder")
|
||||
Expect(isDirIgnored(fsys, ".", dirEntry)).To(BeTrue())
|
||||
})
|
||||
It("returns true when folder name starts with a `.`", func() {
|
||||
dirEntry, _ := getDirEntry(baseDir, ".hidden_folder")
|
||||
Expect(isDirIgnored(baseDir, dirEntry)).To(BeTrue())
|
||||
dirEntry := getDirEntry(baseDir, ".hidden_folder")
|
||||
Expect(isDirIgnored(fsys, ".", dirEntry)).To(BeTrue())
|
||||
})
|
||||
It("returns false when folder name starts with ellipses", func() {
|
||||
dirEntry, _ := getDirEntry(baseDir, "...unhidden_folder")
|
||||
Expect(isDirIgnored(baseDir, dirEntry)).To(BeFalse())
|
||||
dirEntry := getDirEntry(baseDir, "...unhidden_folder")
|
||||
Expect(isDirIgnored(fsys, ".", dirEntry)).To(BeFalse())
|
||||
})
|
||||
It("returns false when folder name is $Recycle.Bin", func() {
|
||||
dirEntry, _ := getDirEntry(baseDir, "$Recycle.Bin")
|
||||
Expect(isDirIgnored(baseDir, dirEntry)).To(BeFalse())
|
||||
dirEntry := getDirEntry(baseDir, "$Recycle.Bin")
|
||||
Expect(isDirIgnored(fsys, ".", dirEntry)).To(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -168,12 +167,12 @@ func (fd *fakeDirFile) ReadDir(n int) ([]fs.DirEntry, error) {
|
||||
return dirs, nil
|
||||
}
|
||||
|
||||
func getDirEntry(baseDir, name string) (os.DirEntry, error) {
|
||||
func getDirEntry(baseDir, name string) os.DirEntry {
|
||||
dirEntries, _ := os.ReadDir(baseDir)
|
||||
for _, entry := range dirEntries {
|
||||
if entry.Name() == name {
|
||||
return entry, nil
|
||||
return entry
|
||||
}
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
panic(fmt.Sprintf("Could not find %s in %s", name, baseDir))
|
||||
}
|
||||
|
||||
157
server/api/api.go
Normal file
157
server/api/api.go
Normal file
@@ -0,0 +1,157 @@
|
||||
//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen -config ./openapi_api.cfg.yaml "../../api/openapi.yaml"
|
||||
//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen -config ./openapi_types.cfg.yaml "../../api/openapi.yaml"
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
middleware "github.com/deepmap/oapi-codegen/pkg/chi-middleware"
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server"
|
||||
)
|
||||
|
||||
var spec = func() *openapi3.T {
|
||||
s, _ := GetSwagger()
|
||||
//s.Servers = nil
|
||||
//s.Components.SecuritySchemes = nil
|
||||
s.Security = nil //TODO
|
||||
return s
|
||||
}()
|
||||
|
||||
func New(ds model.DataStore) *Router {
|
||||
r := &Router{ds: ds}
|
||||
mux := chi.NewRouter()
|
||||
mux.Use(server.Authenticator(ds))
|
||||
mux.Use(server.JWTRefresher)
|
||||
mux.Use(middleware.OapiRequestValidatorWithOptions(spec, &middleware.Options{
|
||||
ErrorHandler: validationErrorHandler,
|
||||
}))
|
||||
|
||||
handler := NewStrictHandlerWithOptions(r, nil, StrictHTTPServerOptions{
|
||||
RequestErrorHandlerFunc: apiErrorHandler,
|
||||
ResponseErrorHandlerFunc: apiErrorHandler,
|
||||
})
|
||||
r.Handler = HandlerWithOptions(handler, ChiServerOptions{
|
||||
BaseRouter: mux,
|
||||
Middlewares: []MiddlewareFunc{storeRequestInContext},
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
var _ StrictServerInterface = (*Router)(nil)
|
||||
|
||||
type Router struct {
|
||||
http.Handler
|
||||
ds model.DataStore
|
||||
}
|
||||
|
||||
func (a *Router) GetServerInfo(_ context.Context, _ GetServerInfoRequestObject) (GetServerInfoResponseObject, error) {
|
||||
return GetServerInfo200JSONResponse{
|
||||
Data: ServerInfo{
|
||||
AuthRequired: true,
|
||||
Features: []ServerInfoFeatures{},
|
||||
Server: consts.AppName,
|
||||
ServerVersion: consts.Version,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *Router) GetTracks(ctx context.Context, request GetTracksRequestObject) (GetTracksResponseObject, error) {
|
||||
options := toQueryOptions(ctx, request.Params)
|
||||
mfs, err := a.ds.MediaFile(ctx).GetAll(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cnt, err := a.ds.MediaFile(ctx).CountAll(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseUrl := baseResourceUrl(ctx, "tracks")
|
||||
links, meta := buildPaginationLinksAndMeta(int32(cnt), request.Params, baseUrl)
|
||||
|
||||
resources := newIncludedResources(ctx, a.ds, request.Params.Include)
|
||||
resources.Artists(mfs.ArtistIDs()...)
|
||||
resources.Albums(mfs.AlbumIDs()...)
|
||||
|
||||
response := GetTracks200JSONResponse{
|
||||
Data: toAPITracks(mfs),
|
||||
Links: links,
|
||||
Meta: &meta,
|
||||
}
|
||||
response.Included, err = resources.Build()
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (a *Router) GetTrack(ctx context.Context, request GetTrackRequestObject) (GetTrackResponseObject, error) {
|
||||
mf, err := a.ds.MediaFile(ctx).Get(request.TrackId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resources := newIncludedResources(ctx, a.ds, request.Params.Include)
|
||||
resources.Artists(mf.ArtistID, mf.AlbumArtistID)
|
||||
resources.Albums(mf.AlbumID)
|
||||
|
||||
response := GetTrack200JSONResponse{
|
||||
Data: toAPITrack(*mf),
|
||||
}
|
||||
response.Included, err = resources.Build()
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (a *Router) GetAlbums(ctx context.Context, request GetAlbumsRequestObject) (GetAlbumsResponseObject, error) {
|
||||
options := toQueryOptions(ctx, request.Params)
|
||||
albums, err := a.ds.Album(ctx).GetAll(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cnt, err := a.ds.MediaFile(ctx).CountAll(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseUrl := baseResourceUrl(ctx, "albums")
|
||||
links, meta := buildPaginationLinksAndMeta(int32(cnt), request.Params, baseUrl)
|
||||
|
||||
resources := newIncludedResources(ctx, a.ds, request.Params.Include)
|
||||
resources.Artists(albums.ArtistIDs()...)
|
||||
|
||||
response := GetAlbums200JSONResponse{
|
||||
Data: toAPIAlbums(albums),
|
||||
Links: links,
|
||||
Meta: &meta,
|
||||
}
|
||||
response.Included, err = resources.Build()
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (a *Router) GetAlbum(ctx context.Context, request GetAlbumRequestObject) (GetAlbumResponseObject, error) {
|
||||
album, err := a.ds.Album(ctx).Get(request.AlbumId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resources := newIncludedResources(ctx, a.ds, request.Params.Include)
|
||||
resources.Artists(album.ArtistID, album.AlbumArtistID)
|
||||
resources.Tracks(album.ID)
|
||||
|
||||
response := GetAlbum200JSONResponse{
|
||||
Data: toAPIAlbum(*album),
|
||||
}
|
||||
response.Included, err = resources.Build()
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (a *Router) GetArtists(ctx context.Context, request GetArtistsRequestObject) (GetArtistsResponseObject, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (a *Router) GetArtist(ctx context.Context, request GetArtistRequestObject) (GetArtistResponseObject, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
17
server/api/api_suite_test.go
Normal file
17
server/api/api_suite_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestApi(t *testing.T) {
|
||||
tests.Init(t, false)
|
||||
log.SetLevel(log.LevelFatal)
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Navidrome JSON:API Suite")
|
||||
}
|
||||
328
server/api/helpers.go
Normal file
328
server/api/helpers.go
Normal file
@@ -0,0 +1,328 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server"
|
||||
. "github.com/navidrome/navidrome/utils/gg"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const requestInContext contextKey = "request"
|
||||
|
||||
type includeSlice []string
|
||||
|
||||
// storeRequestInContext is a middleware function that adds the full request object to the context.
|
||||
func storeRequestInContext(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.WithValue(r.Context(), requestInContext, r)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func toAPITrack(mf model.MediaFile) Track {
|
||||
return Track{
|
||||
Type: ResourceTypeTrack,
|
||||
Id: mf.ID,
|
||||
Attributes: &TrackAttributes{
|
||||
Album: mf.Album,
|
||||
Albumartist: mf.AlbumArtist,
|
||||
Artist: mf.Artist,
|
||||
Bitrate: mf.BitRate,
|
||||
Bpm: P(mf.Bpm),
|
||||
Channels: mf.Channels,
|
||||
Comments: P(mf.Comment),
|
||||
Disc: P(mf.DiscNumber),
|
||||
Duration: mf.Duration,
|
||||
Genre: P(mf.Genre),
|
||||
Mimetype: mf.ContentType(),
|
||||
RecordingMbid: P(mf.MbzTrackID),
|
||||
Size: int(mf.Size),
|
||||
Title: mf.Title,
|
||||
Track: mf.TrackNumber,
|
||||
TrackMbid: P(mf.MbzReleaseTrackID),
|
||||
Year: P(mf.Year),
|
||||
},
|
||||
Relationships: &TrackRelationships{
|
||||
Albums: &[]AlbumTrackRelationship{toAlbumRelationship(mf)},
|
||||
Artists: trackArtistRelationships(mf),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func trackArtistRelationships(mf model.MediaFile) []TrackArtistRelationship {
|
||||
var r []TrackArtistRelationship
|
||||
if mf.AlbumArtistID != "" {
|
||||
r = append(r, toArtistRelationship(mf.AlbumArtistID, ArtistRoleAlbumArtist))
|
||||
}
|
||||
if mf.ArtistID != "" {
|
||||
r = append(r, toArtistRelationship(mf.ArtistID, ArtistRoleArtist))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func toArtistRelationship(id string, artist ArtistRole) TrackArtistRelationship {
|
||||
return TrackArtistRelationship{
|
||||
Data: ResourceObject{
|
||||
Type: ResourceTypeArtist,
|
||||
Id: id,
|
||||
},
|
||||
Meta: ArtistMetaObject{Role: artist},
|
||||
}
|
||||
}
|
||||
|
||||
func toAlbumRelationship(mf model.MediaFile) AlbumTrackRelationship {
|
||||
return AlbumTrackRelationship{
|
||||
Data: ResourceObject{
|
||||
Type: ResourceTypeAlbum,
|
||||
Id: mf.AlbumID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func toAPITracks(mfs model.MediaFiles) []Track {
|
||||
tracks := make([]Track, len(mfs))
|
||||
for i := range mfs {
|
||||
tracks[i] = toAPITrack(mfs[i])
|
||||
}
|
||||
return tracks
|
||||
}
|
||||
|
||||
func toAPIAlbum(ma model.Album) Album {
|
||||
return Album{
|
||||
Type: ResourceTypeAlbum,
|
||||
Id: ma.ID,
|
||||
Attributes: &AlbumAttributes{
|
||||
Artist: ma.AlbumArtist,
|
||||
Genre: P(ma.Genre),
|
||||
ReleaseDate: P(ma.ReleaseDate),
|
||||
Title: ma.Name,
|
||||
Tracktotal: P(ma.SongCount),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func toAPIAlbums(mas model.Albums) []Album {
|
||||
albums := make([]Album, len(mas))
|
||||
for i := range mas {
|
||||
albums[i] = toAPIAlbum(mas[i])
|
||||
}
|
||||
return albums
|
||||
}
|
||||
|
||||
func toAPIArtist(ma model.Artist) Artist {
|
||||
return Artist{
|
||||
Type: ResourceTypeArtist,
|
||||
Id: ma.ID,
|
||||
Attributes: &ArtistAttributes{
|
||||
Name: ma.Name,
|
||||
Bio: P(ma.Biography),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type GetParams interface {
|
||||
GetParams() GetTracksParams
|
||||
}
|
||||
|
||||
func (p GetTracksParams) GetParams() GetTracksParams { return p }
|
||||
|
||||
func (p GetAlbumsParams) GetParams() GetTracksParams { return GetTracksParams(p) }
|
||||
|
||||
// toQueryOptions convert a params struct to a model.QueryOptions struct, to be used by the
|
||||
// GetAll and CountAll functions. It assumes all GetXxxxParams functions have the exact same structure.
|
||||
func toQueryOptions(ctx context.Context, p GetParams) model.QueryOptions {
|
||||
params := p.GetParams()
|
||||
var filters squirrel.And
|
||||
parseFilter := func(fs *[]string, op func(f, v string) squirrel.Sqlizer) {
|
||||
if fs != nil {
|
||||
for _, f := range *fs {
|
||||
parts := strings.SplitN(f, ":", 2)
|
||||
filters = append(filters, op(parts[0], parts[1]))
|
||||
}
|
||||
}
|
||||
}
|
||||
parseFilter(params.FilterEquals, func(f, v string) squirrel.Sqlizer { return squirrel.Eq{f: v} })
|
||||
parseFilter(params.FilterContains, func(f, v string) squirrel.Sqlizer { return squirrel.Like{f: "%" + v + "%"} })
|
||||
parseFilter(params.FilterStartsWith, func(f, v string) squirrel.Sqlizer { return squirrel.Like{f: v + "%"} })
|
||||
parseFilter(params.FilterEndsWith, func(f, v string) squirrel.Sqlizer { return squirrel.Like{f: "%" + v} })
|
||||
parseFilter(params.FilterGreaterThan, func(f, v string) squirrel.Sqlizer { return squirrel.Gt{f: v} })
|
||||
parseFilter(params.FilterGreaterOrEqual, func(f, v string) squirrel.Sqlizer { return squirrel.GtOrEq{f: v} })
|
||||
parseFilter(params.FilterLessThan, func(f, v string) squirrel.Sqlizer { return squirrel.Lt{f: v} })
|
||||
parseFilter(params.FilterLessOrEqual, func(f, v string) squirrel.Sqlizer { return squirrel.LtOrEq{f: v} })
|
||||
offset := V(params.PageOffset)
|
||||
limit := V(params.PageLimit)
|
||||
sort, err := toSortParams(params.Sort)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Ignoring invalid sort parameter", err)
|
||||
}
|
||||
return model.QueryOptions{Max: int(limit), Offset: int(offset), Filters: filters, Sort: sort}
|
||||
}
|
||||
|
||||
var validSortPattern = regexp.MustCompile(`[a-zA-Z0-9_\-]`)
|
||||
|
||||
func toSortParams(sort *string) (string, error) {
|
||||
if sort == nil || *sort == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Split input by comma
|
||||
inputCols := strings.Split(*sort, ",")
|
||||
|
||||
var resultCols []string
|
||||
|
||||
for _, col := range inputCols {
|
||||
trimmedCol := strings.TrimSpace(col)
|
||||
if trimmedCol == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for invalid prefix
|
||||
if !validSortPattern.Match([]byte(string(trimmedCol[0]))) {
|
||||
return "", errors.New("invalid sort parameter: " + trimmedCol)
|
||||
}
|
||||
|
||||
colName := strings.TrimSpace(trimmedCol[1:])
|
||||
// Check for descending order
|
||||
if strings.HasPrefix(trimmedCol, "-") {
|
||||
resultCols = append(resultCols, fmt.Sprintf("%s desc", colName))
|
||||
} else {
|
||||
resultCols = append(resultCols, fmt.Sprintf("%s asc", trimmedCol))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(resultCols, ","), nil
|
||||
}
|
||||
|
||||
func apiErrorHandler(w http.ResponseWriter, _ *http.Request, err error) {
|
||||
var res ErrorObject
|
||||
switch {
|
||||
case errors.Is(err, model.ErrNotAuthorized):
|
||||
res = ErrorObject{Status: P(strconv.Itoa(http.StatusForbidden)), Title: P(http.StatusText(http.StatusForbidden))}
|
||||
case errors.Is(err, model.ErrNotFound):
|
||||
res = ErrorObject{Status: P(strconv.Itoa(http.StatusNotFound)), Title: P(http.StatusText(http.StatusNotFound))}
|
||||
default:
|
||||
res = ErrorObject{Status: P(strconv.Itoa(http.StatusInternalServerError)), Title: P(http.StatusText(http.StatusInternalServerError))}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/vnd.api+json")
|
||||
w.WriteHeader(403)
|
||||
|
||||
_ = json.NewEncoder(w).Encode(ErrorList{[]ErrorObject{res}})
|
||||
}
|
||||
|
||||
func validationErrorHandler(w http.ResponseWriter, message string, statusCode int) {
|
||||
_ = GetTracks400JSONResponse{BadRequestJSONResponse{Errors: []ErrorObject{
|
||||
{
|
||||
Status: P(strconv.Itoa(statusCode)),
|
||||
Title: P(http.StatusText(statusCode)),
|
||||
Detail: P(message),
|
||||
},
|
||||
}}}.VisitGetTracksResponse(w)
|
||||
}
|
||||
|
||||
func buildPaginationLinksAndMeta(totalItems int32, p GetParams, resourceName string) (PaginationLinks, PaginationMeta) {
|
||||
params := p.GetParams()
|
||||
pageLimit := *params.PageLimit
|
||||
pageOffset := *params.PageOffset
|
||||
|
||||
totalPages := (totalItems + pageLimit - 1) / pageLimit
|
||||
currentPage := pageOffset/pageLimit + 1
|
||||
|
||||
meta := PaginationMeta{
|
||||
CurrentPage: ¤tPage,
|
||||
TotalItems: &totalItems,
|
||||
TotalPages: &totalPages,
|
||||
}
|
||||
|
||||
var first, last, next, prev *string
|
||||
|
||||
buildLink := func(page int32) *string {
|
||||
query := url.Values{}
|
||||
query.Add("page[offset]", strconv.Itoa(int(page*pageLimit)))
|
||||
query.Add("page[limit]", strconv.Itoa(int(pageLimit)))
|
||||
|
||||
addFilterParams := func(paramName string, values *[]string) {
|
||||
if values == nil {
|
||||
return
|
||||
}
|
||||
for _, value := range *values {
|
||||
query.Add(paramName, value)
|
||||
}
|
||||
}
|
||||
|
||||
addFilterParams("filter[equals]", params.FilterEquals)
|
||||
addFilterParams("filter[contains]", params.FilterContains)
|
||||
addFilterParams("filter[lessThan]", params.FilterLessThan)
|
||||
addFilterParams("filter[lessOrEqual]", params.FilterLessOrEqual)
|
||||
addFilterParams("filter[greaterThan]", params.FilterGreaterThan)
|
||||
addFilterParams("filter[greaterOrEqual]", params.FilterGreaterOrEqual)
|
||||
addFilterParams("filter[startsWith]", params.FilterStartsWith)
|
||||
addFilterParams("filter[endsWith]", params.FilterEndsWith)
|
||||
|
||||
if params.Sort != nil {
|
||||
query.Add("sort", *params.Sort)
|
||||
}
|
||||
if params.Include != nil {
|
||||
query.Add("include", A(*params.Include))
|
||||
}
|
||||
|
||||
link := resourceName
|
||||
if len(query) > 0 {
|
||||
link += "?" + query.Encode()
|
||||
}
|
||||
return &link
|
||||
}
|
||||
|
||||
if totalPages > 0 {
|
||||
firstLink := buildLink(0)
|
||||
first = firstLink
|
||||
|
||||
lastLink := buildLink(totalPages - 1)
|
||||
last = lastLink
|
||||
}
|
||||
|
||||
if currentPage < totalPages {
|
||||
nextLink := buildLink(currentPage)
|
||||
next = nextLink
|
||||
}
|
||||
|
||||
if currentPage > 1 {
|
||||
prevLink := buildLink(currentPage - 2)
|
||||
prev = prevLink
|
||||
}
|
||||
|
||||
links := PaginationLinks{
|
||||
First: first,
|
||||
Last: last,
|
||||
Next: next,
|
||||
Prev: prev,
|
||||
}
|
||||
|
||||
return links, meta
|
||||
}
|
||||
|
||||
func A[T any](slice []T) string {
|
||||
var buf []string
|
||||
for _, v := range slice {
|
||||
buf = append(buf, fmt.Sprintf("%v", v))
|
||||
}
|
||||
return strings.Join(buf, ",")
|
||||
}
|
||||
|
||||
func baseResourceUrl(ctx context.Context, resourceName string) string {
|
||||
r := ctx.Value(requestInContext).(*http.Request)
|
||||
baseUrl, _ := url.JoinPath(spec.Servers[0].URL, resourceName)
|
||||
return server.AbsoluteURL(r, baseUrl, nil)
|
||||
}
|
||||
185
server/api/helpers_test.go
Normal file
185
server/api/helpers_test.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
|
||||
. "github.com/navidrome/navidrome/utils/gg"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("BuildPaginationLinksAndMeta", func() {
|
||||
var (
|
||||
totalItems int32
|
||||
params GetTracksParams
|
||||
resourceName string
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
totalItems = 150
|
||||
resourceName = "api/resource"
|
||||
})
|
||||
|
||||
Context("with default page limit and offset", func() {
|
||||
BeforeEach(func() {
|
||||
l, o := int32(10), int32(0)
|
||||
params = GetTracksParams{
|
||||
PageLimit: &l,
|
||||
PageOffset: &o,
|
||||
}
|
||||
})
|
||||
|
||||
It("returns correct pagination links and meta", func() {
|
||||
links, meta := buildPaginationLinksAndMeta(totalItems, params, resourceName)
|
||||
|
||||
testLinkEquality(links.First, P("api/resource?page[offset]=0&page[limit]=10"))
|
||||
testLinkEquality(links.Last, P("api/resource?page[offset]=140&page[limit]=10"))
|
||||
testLinkEquality(links.Next, P("api/resource?page[offset]=10&page[limit]=10"))
|
||||
Expect(links.Prev).To(BeNil())
|
||||
|
||||
Expect(meta.CurrentPage).To(Equal(P(int32(1))))
|
||||
Expect(meta.TotalItems).To(Equal(P(int32(150))))
|
||||
Expect(meta.TotalPages).To(Equal(P(int32(15))))
|
||||
})
|
||||
})
|
||||
|
||||
Context("with custom page limit and offset", func() {
|
||||
BeforeEach(func() {
|
||||
params = GetTracksParams{
|
||||
PageLimit: P((PageLimit)(20)),
|
||||
PageOffset: P((PageOffset)(40)),
|
||||
}
|
||||
})
|
||||
|
||||
It("returns correct pagination links and meta", func() {
|
||||
links, meta := buildPaginationLinksAndMeta(totalItems, params, resourceName)
|
||||
|
||||
testLinkEquality(links.First, P("api/resource?page[offset]=0&page[limit]=20"))
|
||||
testLinkEquality(links.Last, P("api/resource?page[offset]=140&page[limit]=20"))
|
||||
testLinkEquality(links.Next, P("api/resource?page[offset]=60&page[limit]=20"))
|
||||
testLinkEquality(links.Prev, P("api/resource?page[offset]=20&page[limit]=20"))
|
||||
|
||||
Expect(meta.CurrentPage).To(Equal(P(int32(3))))
|
||||
Expect(meta.TotalItems).To(Equal(P(int32(150))))
|
||||
Expect(meta.TotalPages).To(Equal(P(int32(8))))
|
||||
})
|
||||
})
|
||||
|
||||
Context("with various filter params", func() {
|
||||
BeforeEach(func() {
|
||||
params = GetTracksParams{
|
||||
PageLimit: P((PageLimit)(20)),
|
||||
PageOffset: P((PageOffset)(40)),
|
||||
FilterEquals: &[]string{"property1:value1", "property2:value2"},
|
||||
FilterContains: &[]string{"property3:value3"},
|
||||
FilterLessThan: &[]string{"property4:value4"},
|
||||
FilterLessOrEqual: &[]string{"property5:value5"},
|
||||
FilterGreaterThan: &[]string{"property6:value6"},
|
||||
FilterGreaterOrEqual: &[]string{"property7:value7"},
|
||||
FilterStartsWith: &[]string{"property8:value8"},
|
||||
FilterEndsWith: &[]string{"property9:value9"},
|
||||
}
|
||||
})
|
||||
|
||||
It("returns correct pagination links with filter params", func() {
|
||||
links, _ := buildPaginationLinksAndMeta(totalItems, params, resourceName)
|
||||
|
||||
validateLink := func(link *string, expectedOffset string) {
|
||||
parsedLink, err := url.Parse(*link)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
queryParams, _ := url.ParseQuery(parsedLink.RawQuery)
|
||||
Expect(queryParams["page[offset]"]).To(ConsistOf(expectedOffset))
|
||||
Expect(queryParams["page[limit]"]).To(ConsistOf("20"))
|
||||
|
||||
for _, param := range *params.FilterEquals {
|
||||
Expect(queryParams["filter[equals]"]).To(ContainElements(param))
|
||||
}
|
||||
for _, param := range *params.FilterContains {
|
||||
Expect(queryParams["filter[contains]"]).To(ContainElement(param))
|
||||
}
|
||||
for _, param := range *params.FilterLessThan {
|
||||
Expect(queryParams["filter[lessThan]"]).To(ContainElement(param))
|
||||
}
|
||||
for _, param := range *params.FilterLessOrEqual {
|
||||
Expect(queryParams["filter[lessOrEqual]"]).To(ContainElement(param))
|
||||
}
|
||||
for _, param := range *params.FilterGreaterThan {
|
||||
Expect(queryParams["filter[greaterThan]"]).To(ContainElement(param))
|
||||
}
|
||||
for _, param := range *params.FilterGreaterOrEqual {
|
||||
Expect(queryParams["filter[greaterOrEqual]"]).To(ContainElement(param))
|
||||
}
|
||||
for _, param := range *params.FilterStartsWith {
|
||||
Expect(queryParams["filter[startsWith]"]).To(ContainElement(param))
|
||||
}
|
||||
for _, param := range *params.FilterEndsWith {
|
||||
Expect(queryParams["filter[endsWith]"]).To(ContainElement(param))
|
||||
}
|
||||
}
|
||||
|
||||
validateLink(links.First, "0")
|
||||
validateLink(links.Last, "140")
|
||||
validateLink(links.Next, "60")
|
||||
validateLink(links.Prev, "20")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("toSortParams", func() {
|
||||
DescribeTable("toSortParams",
|
||||
func(sort string, expected string, expectedError error) {
|
||||
order, err := toSortParams(&sort)
|
||||
Expect(order).To(Equal(expected))
|
||||
if expectedError == nil {
|
||||
Expect(err).To(BeNil())
|
||||
} else {
|
||||
Expect(err).To(Equal(expectedError))
|
||||
}
|
||||
},
|
||||
Entry("should handle nil input", "", "", nil),
|
||||
Entry("should handle empty input", "", "", nil),
|
||||
Entry("should handle single column input", "name", "name asc", nil),
|
||||
Entry("should handle single column input with descending order", "-name", "name desc", nil),
|
||||
Entry("should handle multiple columns input", "name,,date,", "name asc,date asc", nil),
|
||||
Entry("should handle multiple columns input with mixed order and spaces", "name, -age", "name asc,age desc", nil),
|
||||
Entry("should handle relationship columns", "-artist.name", "artist.name desc", nil),
|
||||
Entry("should return an error for invalid input with invalid prefix", "+name", "", errors.New("invalid sort parameter: +name")),
|
||||
Entry("should return an error for invalid prefix in any column", "name,*age", "", errors.New("invalid sort parameter: *age")),
|
||||
)
|
||||
})
|
||||
|
||||
var _ = Describe("storeRequestInContext", func() {
|
||||
var (
|
||||
nextHandler http.Handler
|
||||
handler http.Handler
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
nextHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
Expect(r.Context().Value(requestInContext).(*http.Request).URL).To(Equal(r.URL))
|
||||
})
|
||||
handler = storeRequestInContext(nextHandler)
|
||||
})
|
||||
|
||||
It("adds the full request object to the context", func() {
|
||||
req := httptest.NewRequest("GET", "http://example.com", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
handler.ServeHTTP(resp, req)
|
||||
})
|
||||
})
|
||||
|
||||
func testLinkEquality(link1, link2 *string) {
|
||||
parsedLink1, err := url.Parse(*link1)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
queryParams1, _ := url.ParseQuery(parsedLink1.RawQuery)
|
||||
|
||||
parsedLink2, err := url.Parse(*link2)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
queryParams2, _ := url.ParseQuery(parsedLink2.RawQuery)
|
||||
|
||||
Expect(queryParams1).To(Equal(queryParams2))
|
||||
}
|
||||
123
server/api/includes.go
Normal file
123
server/api/includes.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type includedResources struct {
|
||||
ctx context.Context
|
||||
ds model.DataStore
|
||||
includes *includeSlice
|
||||
resources []IncludedResource
|
||||
ids map[ResourceType][]string
|
||||
}
|
||||
|
||||
func newIncludedResources(ctx context.Context, ds model.DataStore, includes *includeSlice) *includedResources {
|
||||
i := &includedResources{
|
||||
ctx: ctx,
|
||||
ds: ds,
|
||||
includes: includes,
|
||||
}
|
||||
if includes != nil {
|
||||
i.ids = make(map[ResourceType][]string)
|
||||
for _, inc := range *includes {
|
||||
i.ids[ResourceType(inc)] = []string{}
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func (i *includedResources) Tracks(trackIds ...string) {
|
||||
if i.ids == nil || i.ids[ResourceTypeTrack] == nil {
|
||||
return
|
||||
}
|
||||
i.ids[ResourceTypeTrack] = append(i.ids[ResourceTypeTrack], trackIds...)
|
||||
}
|
||||
|
||||
func (i *includedResources) Albums(albumIds ...string) {
|
||||
if i.ids == nil || i.ids[ResourceTypeAlbum] == nil {
|
||||
return
|
||||
}
|
||||
i.ids[ResourceTypeAlbum] = append(i.ids[ResourceTypeAlbum], albumIds...)
|
||||
}
|
||||
|
||||
func (i *includedResources) Artists(artistIds ...string) {
|
||||
if i.ids == nil || i.ids[ResourceTypeArtist] == nil {
|
||||
return
|
||||
}
|
||||
i.ids[ResourceTypeArtist] = append(i.ids[ResourceTypeArtist], artistIds...)
|
||||
}
|
||||
|
||||
func (i *includedResources) addTracks(albumIds []string) error {
|
||||
tracks, err := i.ds.MediaFile(i.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": albumIds}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, tr := range tracks {
|
||||
inc := &IncludedResource{}
|
||||
_ = inc.FromTrack(toAPITrack(tr))
|
||||
i.resources = append(i.resources, *inc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *includedResources) addAlbums(albumIds []string) error {
|
||||
albums, err := i.ds.Album(i.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"id": albumIds}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, al := range albums {
|
||||
inc := &IncludedResource{}
|
||||
_ = inc.FromAlbum(toAPIAlbum(al))
|
||||
i.resources = append(i.resources, *inc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *includedResources) addArtists(artistIds []string) error {
|
||||
artists, err := i.ds.Artist(i.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"artist.id": artistIds}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ar := range artists {
|
||||
inc := &IncludedResource{}
|
||||
_ = inc.FromArtist(toAPIArtist(ar))
|
||||
i.resources = append(i.resources, *inc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *includedResources) Build() (*[]IncludedResource, error) {
|
||||
if i.includes == nil {
|
||||
return nil, nil
|
||||
}
|
||||
for _, typ := range *i.includes {
|
||||
ids := i.ids[ResourceType(typ)]
|
||||
sort.Strings(ids)
|
||||
slices.Compact(ids)
|
||||
if len(ids) == 0 {
|
||||
continue
|
||||
}
|
||||
switch ResourceType(typ) {
|
||||
case ResourceTypeAlbum:
|
||||
if err := i.addAlbums(ids); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case ResourceTypeArtist:
|
||||
if err := i.addArtists(ids); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case ResourceTypeTrack:
|
||||
if err := i.addTracks(ids); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &i.resources, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user