diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index a7ec34c30..fd3c33865 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -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 @@ -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 diff --git a/.gitignore b/.gitignore index 2243b42c0..6a18b5871 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ navidrome.db-wal tags .gitinfo docker-compose.yml -!contrib/docker-compose.yml \ No newline at end of file +!contrib/docker-compose.yml +/api/openapi.yaml diff --git a/Makefile b/Makefile index 43dca10a9..eb3b05df0 100644 --- a/Makefile +++ b/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 diff --git a/api/parameters/_index.yml b/api/parameters/_index.yml new file mode 100644 index 000000000..4280002ed --- /dev/null +++ b/api/parameters/_index.yml @@ -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' diff --git a/api/parameters/query/filterContains.yml b/api/parameters/query/filterContains.yml new file mode 100644 index 000000000..71cee77c4 --- /dev/null +++ b/api/parameters/query/filterContains.yml @@ -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+' diff --git a/api/parameters/query/filterEndsWith.yml b/api/parameters/query/filterEndsWith.yml new file mode 100644 index 000000000..390bd5606 --- /dev/null +++ b/api/parameters/query/filterEndsWith.yml @@ -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+' diff --git a/api/parameters/query/filterEquals.yml b/api/parameters/query/filterEquals.yml new file mode 100644 index 000000000..a523e04b8 --- /dev/null +++ b/api/parameters/query/filterEquals.yml @@ -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+' diff --git a/api/parameters/query/filterGreaterOrEqual.yml b/api/parameters/query/filterGreaterOrEqual.yml new file mode 100644 index 000000000..8a1ed8ec0 --- /dev/null +++ b/api/parameters/query/filterGreaterOrEqual.yml @@ -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+' diff --git a/api/parameters/query/filterGreaterThan.yml b/api/parameters/query/filterGreaterThan.yml new file mode 100644 index 000000000..009a70d19 --- /dev/null +++ b/api/parameters/query/filterGreaterThan.yml @@ -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+' diff --git a/api/parameters/query/filterLessOrEqual.yml b/api/parameters/query/filterLessOrEqual.yml new file mode 100644 index 000000000..33d029983 --- /dev/null +++ b/api/parameters/query/filterLessOrEqual.yml @@ -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+' diff --git a/api/parameters/query/filterLessThan.yml b/api/parameters/query/filterLessThan.yml new file mode 100644 index 000000000..5eb0657f6 --- /dev/null +++ b/api/parameters/query/filterLessThan.yml @@ -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+' diff --git a/api/parameters/query/filterStartsWith.yml b/api/parameters/query/filterStartsWith.yml new file mode 100644 index 000000000..1f7fa0039 --- /dev/null +++ b/api/parameters/query/filterStartsWith.yml @@ -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+' diff --git a/api/parameters/query/include.yml b/api/parameters/query/include.yml new file mode 100644 index 000000000..f3b0a579f --- /dev/null +++ b/api/parameters/query/include.yml @@ -0,0 +1,6 @@ +name: include +in: query +description: Related resources to include in the response, separated by commas +required: false +schema: + type: string diff --git a/api/parameters/query/includeForAlbum.yml b/api/parameters/query/includeForAlbum.yml new file mode 100644 index 000000000..d5c24a46f --- /dev/null +++ b/api/parameters/query/includeForAlbum.yml @@ -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 diff --git a/api/parameters/query/includeForAlbums.yml b/api/parameters/query/includeForAlbums.yml new file mode 100644 index 000000000..411102451 --- /dev/null +++ b/api/parameters/query/includeForAlbums.yml @@ -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 diff --git a/api/parameters/query/includeForTracks.yml b/api/parameters/query/includeForTracks.yml new file mode 100644 index 000000000..2e5b2125f --- /dev/null +++ b/api/parameters/query/includeForTracks.yml @@ -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 diff --git a/api/parameters/query/pageLimit.yml b/api/parameters/query/pageLimit.yml new file mode 100644 index 000000000..7f49946cf --- /dev/null +++ b/api/parameters/query/pageLimit.yml @@ -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 diff --git a/api/parameters/query/pageOffset.yml b/api/parameters/query/pageOffset.yml new file mode 100644 index 000000000..831f6b914 --- /dev/null +++ b/api/parameters/query/pageOffset.yml @@ -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 diff --git a/api/parameters/query/sort.yml b/api/parameters/query/sort.yml new file mode 100644 index 000000000..99e439df4 --- /dev/null +++ b/api/parameters/query/sort.yml @@ -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 diff --git a/api/resources/album.yml b/api/resources/album.yml new file mode 100644 index 000000000..12de5467c --- /dev/null +++ b/api/resources/album.yml @@ -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' diff --git a/api/resources/albums.yml b/api/resources/albums.yml new file mode 100644 index 000000000..e682e4baa --- /dev/null +++ b/api/resources/albums.yml @@ -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' diff --git a/api/resources/artist.yml b/api/resources/artist.yml new file mode 100644 index 000000000..b600c8716 --- /dev/null +++ b/api/resources/artist.yml @@ -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' diff --git a/api/resources/artists.yml b/api/resources/artists.yml new file mode 100644 index 000000000..44db745e8 --- /dev/null +++ b/api/resources/artists.yml @@ -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' diff --git a/api/resources/server.yml b/api/resources/server.yml new file mode 100644 index 000000000..f57467335 --- /dev/null +++ b/api/resources/server.yml @@ -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' diff --git a/api/resources/track.yml b/api/resources/track.yml new file mode 100644 index 000000000..a3fa69034 --- /dev/null +++ b/api/resources/track.yml @@ -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' diff --git a/api/resources/tracks.yml b/api/resources/tracks.yml new file mode 100644 index 000000000..62f71fa55 --- /dev/null +++ b/api/resources/tracks.yml @@ -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' diff --git a/api/responses/BadRequest.yml b/api/responses/BadRequest.yml new file mode 100644 index 000000000..2b3e88ed7 --- /dev/null +++ b/api/responses/BadRequest.yml @@ -0,0 +1,5 @@ +description: Bad Request +content: + application/vnd.api+json: + schema: + $ref: '../schemas/ErrorList.yml' diff --git a/api/responses/InternalServerError.yml b/api/responses/InternalServerError.yml new file mode 100644 index 000000000..8d0f6bded --- /dev/null +++ b/api/responses/InternalServerError.yml @@ -0,0 +1,5 @@ +description: Internal Server Error +content: + application/vnd.api+json: + schema: + $ref: '../schemas/ErrorList.yml' diff --git a/api/responses/NotAuthorized.yml b/api/responses/NotAuthorized.yml new file mode 100644 index 000000000..70a10441c --- /dev/null +++ b/api/responses/NotAuthorized.yml @@ -0,0 +1,5 @@ +description: Not Authorized +content: + application/vnd.api+json: + schema: + $ref: '../schemas/ErrorList.yml' diff --git a/api/responses/NotFound.yml b/api/responses/NotFound.yml new file mode 100644 index 000000000..426976152 --- /dev/null +++ b/api/responses/NotFound.yml @@ -0,0 +1,5 @@ +description: Not Found +content: + application/vnd.api+json: + schema: + $ref: '../schemas/ErrorList.yml' diff --git a/api/responses/_index.yml b/api/responses/_index.yml new file mode 100644 index 000000000..fe735b55a --- /dev/null +++ b/api/responses/_index.yml @@ -0,0 +1,8 @@ +NotFound: + $ref: './NotFound.yml' +NotAuthorized: + $ref: './NotAuthorized.yml' +BadRequest: + $ref: './BadRequest.yml' +InternalServerError: + $ref: './InternalServerError.yml' diff --git a/api/schemas/Album.yml b/api/schemas/Album.yml new file mode 100644 index 000000000..49c959be8 --- /dev/null +++ b/api/schemas/Album.yml @@ -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 diff --git a/api/schemas/AlbumArtistRelationship.yml b/api/schemas/AlbumArtistRelationship.yml new file mode 100644 index 000000000..39b688c15 --- /dev/null +++ b/api/schemas/AlbumArtistRelationship.yml @@ -0,0 +1,9 @@ +type: object +properties: + meta: + $ref: './ArtistMetaObject.yml' + data: + $ref: './ResourceObject.yml' +required: + - meta + - data diff --git a/api/schemas/AlbumAttributes.yml b/api/schemas/AlbumAttributes.yml new file mode 100644 index 000000000..00ce3fb1e --- /dev/null +++ b/api/schemas/AlbumAttributes.yml @@ -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 diff --git a/api/schemas/AlbumTrackRelationship.yml b/api/schemas/AlbumTrackRelationship.yml new file mode 100644 index 000000000..a83f5b2b6 --- /dev/null +++ b/api/schemas/AlbumTrackRelationship.yml @@ -0,0 +1,6 @@ +type: object +properties: + data: + $ref: './ResourceObject.yml' +required: + - data diff --git a/api/schemas/Artist.yml b/api/schemas/Artist.yml new file mode 100644 index 000000000..a9b2102bd --- /dev/null +++ b/api/schemas/Artist.yml @@ -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 diff --git a/api/schemas/ArtistAlbumRelationship.yml b/api/schemas/ArtistAlbumRelationship.yml new file mode 100644 index 000000000..39b688c15 --- /dev/null +++ b/api/schemas/ArtistAlbumRelationship.yml @@ -0,0 +1,9 @@ +type: object +properties: + meta: + $ref: './ArtistMetaObject.yml' + data: + $ref: './ResourceObject.yml' +required: + - meta + - data diff --git a/api/schemas/ArtistAttributes.yml b/api/schemas/ArtistAttributes.yml new file mode 100644 index 000000000..252668a3c --- /dev/null +++ b/api/schemas/ArtistAttributes.yml @@ -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 diff --git a/api/schemas/ArtistMetaObject.yml b/api/schemas/ArtistMetaObject.yml new file mode 100644 index 000000000..7143815f4 --- /dev/null +++ b/api/schemas/ArtistMetaObject.yml @@ -0,0 +1,6 @@ +type: object +properties: + role: + $ref: './ArtistRole.yml' +required: + - role diff --git a/api/schemas/ArtistRole.yml b/api/schemas/ArtistRole.yml new file mode 100644 index 000000000..61106a350 --- /dev/null +++ b/api/schemas/ArtistRole.yml @@ -0,0 +1,5 @@ +type: string +enum: + - artist + - albumArtist +description: The role of an artist in a track or album diff --git a/api/schemas/ArtistTrackRelationship.yml b/api/schemas/ArtistTrackRelationship.yml new file mode 100644 index 000000000..0c3583e10 --- /dev/null +++ b/api/schemas/ArtistTrackRelationship.yml @@ -0,0 +1,14 @@ +type: object +properties: + meta: + type: object + properties: + role: + $ref: './ArtistRole.yml' + required: + - role + data: + $ref: './ResourceObject.yml' +required: + - meta + - data diff --git a/api/schemas/ErrorList.yml b/api/schemas/ErrorList.yml new file mode 100644 index 000000000..4fbaea236 --- /dev/null +++ b/api/schemas/ErrorList.yml @@ -0,0 +1,7 @@ +type: object +required: [errors] +properties: + errors: + type: array + items: + $ref: './ErrorObject.yml' diff --git a/api/schemas/ErrorObject.yml b/api/schemas/ErrorObject.yml new file mode 100644 index 000000000..bc13d16f3 --- /dev/null +++ b/api/schemas/ErrorObject.yml @@ -0,0 +1,10 @@ +type: object +properties: + id: + type: string + status: + type: string + title: + type: string + detail: + type: string diff --git a/api/schemas/IncludedResource.yml b/api/schemas/IncludedResource.yml new file mode 100644 index 000000000..8887e0554 --- /dev/null +++ b/api/schemas/IncludedResource.yml @@ -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' diff --git a/api/schemas/PaginationLinks.yml b/api/schemas/PaginationLinks.yml new file mode 100644 index 000000000..31690c6a8 --- /dev/null +++ b/api/schemas/PaginationLinks.yml @@ -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 diff --git a/api/schemas/PaginationMeta.yml b/api/schemas/PaginationMeta.yml new file mode 100644 index 000000000..877237652 --- /dev/null +++ b/api/schemas/PaginationMeta.yml @@ -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 diff --git a/api/schemas/ResourceList.yml b/api/schemas/ResourceList.yml new file mode 100644 index 000000000..45fc844b3 --- /dev/null +++ b/api/schemas/ResourceList.yml @@ -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' diff --git a/api/schemas/ResourceObject.yml b/api/schemas/ResourceObject.yml new file mode 100644 index 000000000..adbc88140 --- /dev/null +++ b/api/schemas/ResourceObject.yml @@ -0,0 +1,8 @@ +type: object +required: [id, type] +properties: + id: + type: string + description: The unique identifier for the resource + type: + $ref: './ResourceType.yml' diff --git a/api/schemas/ResourceType.yml b/api/schemas/ResourceType.yml new file mode 100644 index 000000000..70b12e7ec --- /dev/null +++ b/api/schemas/ResourceType.yml @@ -0,0 +1,6 @@ +type: string +description: The type of the resource +enum: + - album + - artist + - track diff --git a/api/schemas/ServerInfo.yml b/api/schemas/ServerInfo.yml new file mode 100644 index 000000000..58e7379e6 --- /dev/null +++ b/api/schemas/ServerInfo.yml @@ -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 diff --git a/api/schemas/Track.yml b/api/schemas/Track.yml new file mode 100644 index 000000000..bd730fae7 --- /dev/null +++ b/api/schemas/Track.yml @@ -0,0 +1,8 @@ +allOf: + - $ref: './ResourceObject.yml' + - type: object + properties: + attributes: + $ref: './TrackAttributes.yml' + relationships: + $ref: './TrackRelationships.yml' diff --git a/api/schemas/TrackArtistRelationship.yml b/api/schemas/TrackArtistRelationship.yml new file mode 100644 index 000000000..39b688c15 --- /dev/null +++ b/api/schemas/TrackArtistRelationship.yml @@ -0,0 +1,9 @@ +type: object +properties: + meta: + $ref: './ArtistMetaObject.yml' + data: + $ref: './ResourceObject.yml' +required: + - meta + - data diff --git a/api/schemas/TrackAttributes.yml b/api/schemas/TrackAttributes.yml new file mode 100644 index 000000000..bb238e41d --- /dev/null +++ b/api/schemas/TrackAttributes.yml @@ -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. diff --git a/api/schemas/TrackRelationships.yml b/api/schemas/TrackRelationships.yml new file mode 100644 index 000000000..43a69a712 --- /dev/null +++ b/api/schemas/TrackRelationships.yml @@ -0,0 +1,12 @@ +type: object +properties: + artists: + type: array + items: + $ref: './TrackArtistRelationship.yml' + albums: + type: array + items: + $ref: './AlbumTrackRelationship.yml' +required: + - artists diff --git a/api/schemas/_index.yml b/api/schemas/_index.yml new file mode 100644 index 000000000..cb371569f --- /dev/null +++ b/api/schemas/_index.yml @@ -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' diff --git a/api/spec.yml b/api/spec.yml new file mode 100644 index 000000000..58f671aad --- /dev/null +++ b/api/spec.yml @@ -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 diff --git a/cmd/root.go b/cmd/root.go index d9fd9d614..415cf12f3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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) } } diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 8fc241985..47c01d948 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -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 ( diff --git a/cmd/wire_injectors.go b/cmd/wire_injectors.go index cc896421f..ad444ee5c 100644 --- a/cmd/wire_injectors.go +++ b/cmd/wire_injectors.go @@ -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, diff --git a/consts/consts.go b/consts/consts.go index d169661dd..6491479bc 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -32,6 +32,7 @@ const ( URLPathUI = "/app" URLPathNativeAPI = "/api" + URLPathAPI = "/api/v2" URLPathSubsonicAPI = "/rest" URLPathPublic = "/share" URLPathPublicImages = URLPathPublic + "/img" diff --git a/go.mod b/go.mod index e8f77756d..27c4b0e63 100644 --- a/go.mod +++ b/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,9 +18,10 @@ 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 @@ -29,62 +31,87 @@ require ( 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.24 - github.com/mileusna/useragent v1.3.2 - github.com/onsi/ginkgo/v2 v2.9.5 - github.com/onsi/gomega v1.27.7 - github.com/pressly/goose/v3 v3.11.2 - github.com/prometheus/client_golang v1.15.1 + github.com/microcosm-cc/bluemonday v1.0.23 + 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.10.0 + github.com/prometheus/client_golang v1.14.0 github.com/robfig/cron/v3 v3.0.1 - github.com/sirupsen/logrus v1.9.2 + github.com/sirupsen/logrus v1.9.0 github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.15.0 - github.com/stretchr/testify v1.8.3 + github.com/stretchr/testify v1.8.2 github.com/unrolled/secure v1.13.0 github.com/xrash/smetrics v0.0.0-20200730060457-89a2a8a1fb0b golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e golang.org/x/image v0.7.0 - golang.org/x/sync v0.2.0 + golang.org/x/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 @@ -94,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 ) diff --git a/go.sum b/go.sum index 1b8ac6b5d..bfdaa2678 100644 --- a/go.sum +++ b/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,14 +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/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= @@ -219,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= @@ -236,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/microcosm-cc/bluemonday v1.0.24 h1:NGQoPtwGVcbGkKfvyYk1yRqknzBuoMiUrO6R7uFTPlw= -github.com/microcosm-cc/bluemonday v1.0.24/go.mod h1:ArQySAMps0790cHSkdPEJ7bGkF2VePWH773hsJNSHf8= -github.com/mileusna/useragent v1.3.2 h1:yGBQVNkyrlnSe4l0rlaQoH8XlG9xDkc6a7ygwPxALoU= -github.com/mileusna/useragent v1.3.2/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= +github.com/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.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= @@ -262,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.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= -github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/ginkgo/v2 v2.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.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= -github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= +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= @@ -296,9 +417,11 @@ 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.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= -github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +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= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= @@ -315,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= @@ -328,13 +453,23 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/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= @@ -352,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= @@ -360,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= @@ -409,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= @@ -416,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= @@ -440,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= @@ -454,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= @@ -466,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= @@ -486,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= @@ -498,6 +644,8 @@ 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= @@ -506,19 +654,28 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w 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= @@ -529,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= @@ -592,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= @@ -688,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= @@ -699,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= @@ -712,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= diff --git a/model/album.go b/model/album.go index cae0f427a..3d2c9b7d1 100644 --- a/model/album.go +++ b/model/album.go @@ -60,6 +60,10 @@ 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"` ReleaseDate string `json:"releaseDate"` @@ -91,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) diff --git a/model/mediafile.go b/model/mediafile.go index 9caf54e94..891193e80 100644 --- a/model/mediafile.go +++ b/model/mediafile.go @@ -183,6 +183,22 @@ 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 { diff --git a/persistence/sql_base_repository.go b/persistence/sql_base_repository.go index d1ecf2d18..fa8938f45 100644 --- a/persistence/sql_base_repository.go +++ b/persistence/sql_base_repository.go @@ -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,23 +97,6 @@ func (r sqlRepository) buildSortOrder(sort, order string) string { return strings.Join(newSort, ", ") } -func splitFunc(delimiter rune) func(c rune) bool { - open := 0 - return func(c rune) bool { - if c == '(' { - open++ - return false - } - if open > 0 { - if c == ')' { - open-- - } - 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) diff --git a/server/api/api.go b/server/api/api.go new file mode 100644 index 000000000..011825747 --- /dev/null +++ b/server/api/api.go @@ -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") +} diff --git a/server/api/api_suite_test.go b/server/api/api_suite_test.go new file mode 100644 index 000000000..a322cebba --- /dev/null +++ b/server/api/api_suite_test.go @@ -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") +} diff --git a/server/api/helpers.go b/server/api/helpers.go new file mode 100644 index 000000000..5eed3ec18 --- /dev/null +++ b/server/api/helpers.go @@ -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) +} diff --git a/server/api/helpers_test.go b/server/api/helpers_test.go new file mode 100644 index 000000000..fddb7b70f --- /dev/null +++ b/server/api/helpers_test.go @@ -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)) +} diff --git a/server/api/includes.go b/server/api/includes.go new file mode 100644 index 000000000..ffd86ba39 --- /dev/null +++ b/server/api/includes.go @@ -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 +} diff --git a/server/api/openapi_api.cfg.yaml b/server/api/openapi_api.cfg.yaml new file mode 100644 index 000000000..e046b5986 --- /dev/null +++ b/server/api/openapi_api.cfg.yaml @@ -0,0 +1,6 @@ +package: api +generate: + chi-server: true + strict-server: true + embedded-spec: true +output: openapi_api.gen.go diff --git a/server/api/openapi_api.gen.go b/server/api/openapi_api.gen.go new file mode 100644 index 000000000..755ec5313 --- /dev/null +++ b/server/api/openapi_api.gen.go @@ -0,0 +1,1401 @@ +// Package api provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version v1.12.5-0.20230513000919-14548c7e7bbe DO NOT EDIT. +package api + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "net/url" + "path" + "strings" + + "github.com/deepmap/oapi-codegen/pkg/runtime" + "github.com/getkin/kin-openapi/openapi3" + "github.com/go-chi/chi/v5" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Retrieve a list of albums + // (GET /albums) + GetAlbums(w http.ResponseWriter, r *http.Request, params GetAlbumsParams) + // Retrieve an individual album + // (GET /albums/{albumId}) + GetAlbum(w http.ResponseWriter, r *http.Request, albumId string, params GetAlbumParams) + // Retrieve a list of artists + // (GET /artists) + GetArtists(w http.ResponseWriter, r *http.Request, params GetArtistsParams) + // Retrieve an individual artist + // (GET /artists/{artistId}) + GetArtist(w http.ResponseWriter, r *http.Request, artistId string, params GetArtistParams) + // Get server's global info + // (GET /server) + GetServerInfo(w http.ResponseWriter, r *http.Request) + // Retrieve a list of tracks + // (GET /tracks) + GetTracks(w http.ResponseWriter, r *http.Request, params GetTracksParams) + // Retrieve an individual track + // (GET /tracks/{trackId}) + GetTrack(w http.ResponseWriter, r *http.Request, trackId string, params GetTrackParams) +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +type MiddlewareFunc func(http.Handler) http.Handler + +// GetAlbums operation middleware +func (siw *ServerInterfaceWrapper) GetAlbums(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params GetAlbumsParams + + // ------------- Optional query parameter "page[limit]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "page[limit]", r.URL.Query(), ¶ms.PageLimit) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page[limit]", Err: err}) + return + } + + // ------------- Optional query parameter "page[offset]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "page[offset]", r.URL.Query(), ¶ms.PageOffset) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page[offset]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[equals]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[equals]", r.URL.Query(), ¶ms.FilterEquals) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[equals]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[contains]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[contains]", r.URL.Query(), ¶ms.FilterContains) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[contains]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[lessThan]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[lessThan]", r.URL.Query(), ¶ms.FilterLessThan) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[lessThan]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[lessOrEqual]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[lessOrEqual]", r.URL.Query(), ¶ms.FilterLessOrEqual) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[lessOrEqual]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[greaterThan]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[greaterThan]", r.URL.Query(), ¶ms.FilterGreaterThan) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[greaterThan]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[greaterOrEqual]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[greaterOrEqual]", r.URL.Query(), ¶ms.FilterGreaterOrEqual) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[greaterOrEqual]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[startsWith]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[startsWith]", r.URL.Query(), ¶ms.FilterStartsWith) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[startsWith]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[endsWith]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[endsWith]", r.URL.Query(), ¶ms.FilterEndsWith) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[endsWith]", Err: err}) + return + } + + // ------------- Optional query parameter "sort" ------------- + + err = runtime.BindQueryParameter("form", true, false, "sort", r.URL.Query(), ¶ms.Sort) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "sort", Err: err}) + return + } + + // ------------- Optional query parameter "include" ------------- + + err = runtime.BindQueryParameter("form", false, false, "include", r.URL.Query(), ¶ms.Include) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "include", Err: err}) + return + } + + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetAlbums(w, r, params) + }) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// GetAlbum operation middleware +func (siw *ServerInterfaceWrapper) GetAlbum(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "albumId" ------------- + var albumId string + + err = runtime.BindStyledParameterWithLocation("simple", false, "albumId", runtime.ParamLocationPath, chi.URLParam(r, "albumId"), &albumId) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "albumId", Err: err}) + return + } + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params GetAlbumParams + + // ------------- Optional query parameter "include" ------------- + + err = runtime.BindQueryParameter("form", false, false, "include", r.URL.Query(), ¶ms.Include) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "include", Err: err}) + return + } + + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetAlbum(w, r, albumId, params) + }) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// GetArtists operation middleware +func (siw *ServerInterfaceWrapper) GetArtists(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params GetArtistsParams + + // ------------- Optional query parameter "page[limit]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "page[limit]", r.URL.Query(), ¶ms.PageLimit) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page[limit]", Err: err}) + return + } + + // ------------- Optional query parameter "page[offset]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "page[offset]", r.URL.Query(), ¶ms.PageOffset) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page[offset]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[equals]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[equals]", r.URL.Query(), ¶ms.FilterEquals) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[equals]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[contains]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[contains]", r.URL.Query(), ¶ms.FilterContains) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[contains]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[lessThan]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[lessThan]", r.URL.Query(), ¶ms.FilterLessThan) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[lessThan]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[lessOrEqual]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[lessOrEqual]", r.URL.Query(), ¶ms.FilterLessOrEqual) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[lessOrEqual]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[greaterThan]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[greaterThan]", r.URL.Query(), ¶ms.FilterGreaterThan) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[greaterThan]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[greaterOrEqual]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[greaterOrEqual]", r.URL.Query(), ¶ms.FilterGreaterOrEqual) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[greaterOrEqual]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[startsWith]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[startsWith]", r.URL.Query(), ¶ms.FilterStartsWith) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[startsWith]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[endsWith]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[endsWith]", r.URL.Query(), ¶ms.FilterEndsWith) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[endsWith]", Err: err}) + return + } + + // ------------- Optional query parameter "sort" ------------- + + err = runtime.BindQueryParameter("form", true, false, "sort", r.URL.Query(), ¶ms.Sort) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "sort", Err: err}) + return + } + + // ------------- Optional query parameter "include" ------------- + + err = runtime.BindQueryParameter("form", true, false, "include", r.URL.Query(), ¶ms.Include) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "include", Err: err}) + return + } + + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetArtists(w, r, params) + }) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// GetArtist operation middleware +func (siw *ServerInterfaceWrapper) GetArtist(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "artistId" ------------- + var artistId string + + err = runtime.BindStyledParameterWithLocation("simple", false, "artistId", runtime.ParamLocationPath, chi.URLParam(r, "artistId"), &artistId) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "artistId", Err: err}) + return + } + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params GetArtistParams + + // ------------- Optional query parameter "include" ------------- + + err = runtime.BindQueryParameter("form", true, false, "include", r.URL.Query(), ¶ms.Include) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "include", Err: err}) + return + } + + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetArtist(w, r, artistId, params) + }) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// GetServerInfo operation middleware +func (siw *ServerInterfaceWrapper) GetServerInfo(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetServerInfo(w, r) + }) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// GetTracks operation middleware +func (siw *ServerInterfaceWrapper) GetTracks(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params GetTracksParams + + // ------------- Optional query parameter "page[limit]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "page[limit]", r.URL.Query(), ¶ms.PageLimit) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page[limit]", Err: err}) + return + } + + // ------------- Optional query parameter "page[offset]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "page[offset]", r.URL.Query(), ¶ms.PageOffset) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page[offset]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[equals]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[equals]", r.URL.Query(), ¶ms.FilterEquals) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[equals]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[contains]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[contains]", r.URL.Query(), ¶ms.FilterContains) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[contains]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[lessThan]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[lessThan]", r.URL.Query(), ¶ms.FilterLessThan) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[lessThan]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[lessOrEqual]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[lessOrEqual]", r.URL.Query(), ¶ms.FilterLessOrEqual) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[lessOrEqual]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[greaterThan]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[greaterThan]", r.URL.Query(), ¶ms.FilterGreaterThan) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[greaterThan]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[greaterOrEqual]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[greaterOrEqual]", r.URL.Query(), ¶ms.FilterGreaterOrEqual) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[greaterOrEqual]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[startsWith]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[startsWith]", r.URL.Query(), ¶ms.FilterStartsWith) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[startsWith]", Err: err}) + return + } + + // ------------- Optional query parameter "filter[endsWith]" ------------- + + err = runtime.BindQueryParameter("form", true, false, "filter[endsWith]", r.URL.Query(), ¶ms.FilterEndsWith) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter[endsWith]", Err: err}) + return + } + + // ------------- Optional query parameter "sort" ------------- + + err = runtime.BindQueryParameter("form", true, false, "sort", r.URL.Query(), ¶ms.Sort) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "sort", Err: err}) + return + } + + // ------------- Optional query parameter "include" ------------- + + err = runtime.BindQueryParameter("form", false, false, "include", r.URL.Query(), ¶ms.Include) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "include", Err: err}) + return + } + + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetTracks(w, r, params) + }) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// GetTrack operation middleware +func (siw *ServerInterfaceWrapper) GetTrack(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "trackId" ------------- + var trackId string + + err = runtime.BindStyledParameterWithLocation("simple", false, "trackId", runtime.ParamLocationPath, chi.URLParam(r, "trackId"), &trackId) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "trackId", Err: err}) + return + } + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params GetTrackParams + + // ------------- Optional query parameter "include" ------------- + + err = runtime.BindQueryParameter("form", false, false, "include", r.URL.Query(), ¶ms.Include) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "include", Err: err}) + return + } + + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetTrack(w, r, trackId, params) + }) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +type UnescapedCookieParamError struct { + ParamName string + Err error +} + +func (e *UnescapedCookieParamError) Error() string { + return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName) +} + +func (e *UnescapedCookieParamError) Unwrap() error { + return e.Err +} + +type UnmarshallingParamError struct { + ParamName string + Err error +} + +func (e *UnmarshallingParamError) Error() string { + return fmt.Sprintf("Error unmarshalling parameter %s as JSON: %s", e.ParamName, e.Err.Error()) +} + +func (e *UnmarshallingParamError) Unwrap() error { + return e.Err +} + +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +type RequiredHeaderError struct { + ParamName string + Err error +} + +func (e *RequiredHeaderError) Error() string { + return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName) +} + +func (e *RequiredHeaderError) Unwrap() error { + return e.Err +} + +type InvalidParamFormatError struct { + ParamName string + Err error +} + +func (e *InvalidParamFormatError) Error() string { + return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error()) +} + +func (e *InvalidParamFormatError) Unwrap() error { + return e.Err +} + +type TooManyValuesForParamError struct { + ParamName string + Count int +} + +func (e *TooManyValuesForParamError) Error() string { + return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count) +} + +// Handler creates http.Handler with routing matching OpenAPI spec. +func Handler(si ServerInterface) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{}) +} + +type ChiServerOptions struct { + BaseURL string + BaseRouter chi.Router + Middlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. +func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{ + BaseRouter: r, + }) +} + +func HandlerFromMuxWithBaseURL(si ServerInterface, r chi.Router, baseURL string) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{ + BaseURL: baseURL, + BaseRouter: r, + }) +} + +// HandlerWithOptions creates http.Handler with additional options +func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler { + r := options.BaseRouter + + if r == nil { + r = chi.NewRouter() + } + if options.ErrorHandlerFunc == nil { + options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + } + } + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandlerFunc: options.ErrorHandlerFunc, + } + + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/albums", wrapper.GetAlbums) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/albums/{albumId}", wrapper.GetAlbum) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/artists", wrapper.GetArtists) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/artists/{artistId}", wrapper.GetArtist) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/server", wrapper.GetServerInfo) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/tracks", wrapper.GetTracks) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/tracks/{trackId}", wrapper.GetTrack) + }) + + return r +} + +type BadRequestJSONResponse ErrorList + +type InternalServerErrorJSONResponse ErrorList + +type NotAuthorizedJSONResponse ErrorList + +type NotFoundJSONResponse ErrorList + +type GetAlbumsRequestObject struct { + Params GetAlbumsParams +} + +type GetAlbumsResponseObject interface { + VisitGetAlbumsResponse(w http.ResponseWriter) error +} + +type GetAlbums200JSONResponse struct { + Data []Album `json:"data"` + + // Included Included resources, as requested by the `include` query parameter + Included *[]IncludedResource `json:"included,omitempty"` + Links PaginationLinks `json:"links"` + Meta *PaginationMeta `json:"meta,omitempty"` +} + +func (response GetAlbums200JSONResponse) VisitGetAlbumsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetAlbums400JSONResponse struct{ BadRequestJSONResponse } + +func (response GetAlbums400JSONResponse) VisitGetAlbumsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type GetAlbums403JSONResponse struct{ NotAuthorizedJSONResponse } + +func (response GetAlbums403JSONResponse) VisitGetAlbumsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + +type GetAlbums500JSONResponse struct { + InternalServerErrorJSONResponse +} + +func (response GetAlbums500JSONResponse) VisitGetAlbumsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetAlbumRequestObject struct { + AlbumId string `json:"albumId"` + Params GetAlbumParams +} + +type GetAlbumResponseObject interface { + VisitGetAlbumResponse(w http.ResponseWriter) error +} + +type GetAlbum200JSONResponse struct { + Data Album `json:"data"` + + // Included Included resources, as requested by the `include` query parameter + Included *[]IncludedResource `json:"included,omitempty"` +} + +func (response GetAlbum200JSONResponse) VisitGetAlbumResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetAlbum403JSONResponse struct{ NotAuthorizedJSONResponse } + +func (response GetAlbum403JSONResponse) VisitGetAlbumResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + +type GetAlbum404JSONResponse struct{ NotFoundJSONResponse } + +func (response GetAlbum404JSONResponse) VisitGetAlbumResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type GetAlbum500JSONResponse struct { + InternalServerErrorJSONResponse +} + +func (response GetAlbum500JSONResponse) VisitGetAlbumResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetArtistsRequestObject struct { + Params GetArtistsParams +} + +type GetArtistsResponseObject interface { + VisitGetArtistsResponse(w http.ResponseWriter) error +} + +type GetArtists200JSONResponse struct { + Data []Artist `json:"data"` + Links PaginationLinks `json:"links"` + Meta *PaginationMeta `json:"meta,omitempty"` +} + +func (response GetArtists200JSONResponse) VisitGetArtistsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetArtists400JSONResponse struct{ BadRequestJSONResponse } + +func (response GetArtists400JSONResponse) VisitGetArtistsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type GetArtists403JSONResponse struct{ NotAuthorizedJSONResponse } + +func (response GetArtists403JSONResponse) VisitGetArtistsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + +type GetArtists500JSONResponse struct { + InternalServerErrorJSONResponse +} + +func (response GetArtists500JSONResponse) VisitGetArtistsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetArtistRequestObject struct { + ArtistId string `json:"artistId"` + Params GetArtistParams +} + +type GetArtistResponseObject interface { + VisitGetArtistResponse(w http.ResponseWriter) error +} + +type GetArtist200JSONResponse struct { + Data Artist `json:"data"` +} + +func (response GetArtist200JSONResponse) VisitGetArtistResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetArtist403JSONResponse struct{ NotAuthorizedJSONResponse } + +func (response GetArtist403JSONResponse) VisitGetArtistResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + +type GetArtist404JSONResponse struct{ NotFoundJSONResponse } + +func (response GetArtist404JSONResponse) VisitGetArtistResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type GetArtist500JSONResponse struct { + InternalServerErrorJSONResponse +} + +func (response GetArtist500JSONResponse) VisitGetArtistResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetServerInfoRequestObject struct { +} + +type GetServerInfoResponseObject interface { + VisitGetServerInfoResponse(w http.ResponseWriter) error +} + +type GetServerInfo200JSONResponse struct { + Data ServerInfo `json:"data"` +} + +func (response GetServerInfo200JSONResponse) VisitGetServerInfoResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetServerInfo403JSONResponse struct{ NotAuthorizedJSONResponse } + +func (response GetServerInfo403JSONResponse) VisitGetServerInfoResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + +type GetServerInfo500JSONResponse struct { + InternalServerErrorJSONResponse +} + +func (response GetServerInfo500JSONResponse) VisitGetServerInfoResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetTracksRequestObject struct { + Params GetTracksParams +} + +type GetTracksResponseObject interface { + VisitGetTracksResponse(w http.ResponseWriter) error +} + +type GetTracks200JSONResponse struct { + Data []Track `json:"data"` + + // Included Included resources, as requested by the `include` query parameter + Included *[]IncludedResource `json:"included,omitempty"` + Links PaginationLinks `json:"links"` + Meta *PaginationMeta `json:"meta,omitempty"` +} + +func (response GetTracks200JSONResponse) VisitGetTracksResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetTracks400JSONResponse struct{ BadRequestJSONResponse } + +func (response GetTracks400JSONResponse) VisitGetTracksResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type GetTracks403JSONResponse struct{ NotAuthorizedJSONResponse } + +func (response GetTracks403JSONResponse) VisitGetTracksResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + +type GetTracks500JSONResponse struct { + InternalServerErrorJSONResponse +} + +func (response GetTracks500JSONResponse) VisitGetTracksResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetTrackRequestObject struct { + TrackId string `json:"trackId"` + Params GetTrackParams +} + +type GetTrackResponseObject interface { + VisitGetTrackResponse(w http.ResponseWriter) error +} + +type GetTrack200JSONResponse struct { + Data Track `json:"data"` + + // Included Included resources, as requested by the `include` query parameter + Included *[]IncludedResource `json:"included,omitempty"` +} + +func (response GetTrack200JSONResponse) VisitGetTrackResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetTrack403JSONResponse struct{ NotAuthorizedJSONResponse } + +func (response GetTrack403JSONResponse) VisitGetTrackResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + +type GetTrack404JSONResponse struct{ NotFoundJSONResponse } + +func (response GetTrack404JSONResponse) VisitGetTrackResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type GetTrack500JSONResponse struct { + InternalServerErrorJSONResponse +} + +func (response GetTrack500JSONResponse) VisitGetTrackResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/vnd.api+json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + // Retrieve a list of albums + // (GET /albums) + GetAlbums(ctx context.Context, request GetAlbumsRequestObject) (GetAlbumsResponseObject, error) + // Retrieve an individual album + // (GET /albums/{albumId}) + GetAlbum(ctx context.Context, request GetAlbumRequestObject) (GetAlbumResponseObject, error) + // Retrieve a list of artists + // (GET /artists) + GetArtists(ctx context.Context, request GetArtistsRequestObject) (GetArtistsResponseObject, error) + // Retrieve an individual artist + // (GET /artists/{artistId}) + GetArtist(ctx context.Context, request GetArtistRequestObject) (GetArtistResponseObject, error) + // Get server's global info + // (GET /server) + GetServerInfo(ctx context.Context, request GetServerInfoRequestObject) (GetServerInfoResponseObject, error) + // Retrieve a list of tracks + // (GET /tracks) + GetTracks(ctx context.Context, request GetTracksRequestObject) (GetTracksResponseObject, error) + // Retrieve an individual track + // (GET /tracks/{trackId}) + GetTrack(ctx context.Context, request GetTrackRequestObject) (GetTrackResponseObject, error) +} + +type StrictHandlerFunc = runtime.StrictHttpHandlerFunc +type StrictMiddlewareFunc = runtime.StrictHttpMiddlewareFunc + +type StrictHTTPServerOptions struct { + RequestErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) + ResponseErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares, options: StrictHTTPServerOptions{ + RequestErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + }, + ResponseErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusInternalServerError) + }, + }} +} + +func NewStrictHandlerWithOptions(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc, options StrictHTTPServerOptions) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares, options: options} +} + +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc + options StrictHTTPServerOptions +} + +// GetAlbums operation middleware +func (sh *strictHandler) GetAlbums(w http.ResponseWriter, r *http.Request, params GetAlbumsParams) { + var request GetAlbumsRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetAlbums(ctx, request.(GetAlbumsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetAlbums") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetAlbumsResponseObject); ok { + if err := validResponse.VisitGetAlbumsResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("Unexpected response type: %T", response)) + } +} + +// GetAlbum operation middleware +func (sh *strictHandler) GetAlbum(w http.ResponseWriter, r *http.Request, albumId string, params GetAlbumParams) { + var request GetAlbumRequestObject + + request.AlbumId = albumId + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetAlbum(ctx, request.(GetAlbumRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetAlbum") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetAlbumResponseObject); ok { + if err := validResponse.VisitGetAlbumResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("Unexpected response type: %T", response)) + } +} + +// GetArtists operation middleware +func (sh *strictHandler) GetArtists(w http.ResponseWriter, r *http.Request, params GetArtistsParams) { + var request GetArtistsRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetArtists(ctx, request.(GetArtistsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetArtists") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetArtistsResponseObject); ok { + if err := validResponse.VisitGetArtistsResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("Unexpected response type: %T", response)) + } +} + +// GetArtist operation middleware +func (sh *strictHandler) GetArtist(w http.ResponseWriter, r *http.Request, artistId string, params GetArtistParams) { + var request GetArtistRequestObject + + request.ArtistId = artistId + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetArtist(ctx, request.(GetArtistRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetArtist") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetArtistResponseObject); ok { + if err := validResponse.VisitGetArtistResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("Unexpected response type: %T", response)) + } +} + +// GetServerInfo operation middleware +func (sh *strictHandler) GetServerInfo(w http.ResponseWriter, r *http.Request) { + var request GetServerInfoRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetServerInfo(ctx, request.(GetServerInfoRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetServerInfo") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetServerInfoResponseObject); ok { + if err := validResponse.VisitGetServerInfoResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("Unexpected response type: %T", response)) + } +} + +// GetTracks operation middleware +func (sh *strictHandler) GetTracks(w http.ResponseWriter, r *http.Request, params GetTracksParams) { + var request GetTracksRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetTracks(ctx, request.(GetTracksRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetTracks") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetTracksResponseObject); ok { + if err := validResponse.VisitGetTracksResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("Unexpected response type: %T", response)) + } +} + +// GetTrack operation middleware +func (sh *strictHandler) GetTrack(w http.ResponseWriter, r *http.Request, trackId string, params GetTrackParams) { + var request GetTrackRequestObject + + request.TrackId = trackId + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetTrack(ctx, request.(GetTrackRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetTrack") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetTrackResponseObject); ok { + if err := validResponse.VisitGetTrackResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("Unexpected response type: %T", response)) + } +} + +// Base64 encoded, gzipped, json marshaled Swagger object +var swaggerSpec = []string{ + + "H4sIAAAAAAAC/+w73W7bONavQuj7gJnBqHa2nd2LAHuRzrZFBm0aJJntRSeLoaRji1OJVEnKiacwsK+x", + "r7dPsuAhKckWZctu0p+gN20i8vzy8Pzx5EOUirISHLhW0fGHqKKSlqBB4m8zVmiQPwuuKeP4JQOVSlZp", + "Jnh0HD3HdZIsCeVLUklRgdRLkloAxudEw62ekF8VncMxsejeumV1/XcPcbygRQ1RHDGD9X0NchnFEacl", + "RMfRJlQURyrNoaSGH6ahdIxrDdKA/+u3325+PDb/RHGkl5XBobRkfB6tmg9USrqMVqvYoX/GM/WG6Xy0", + "jDqnmgDPFLlhOg/KCQ7nfnI2UPcj5/uaFuNPEmWjnMAtTTUpqU7znpSIcU8ZLcy9SPhCAtUgX1tRd0nK", + "6xIkS1uJ5xbcnC8nQhJklWhBKEHBNsWfr5HbTw0bsGPVkR2gjquc8o/TxXb5Df6DhEfAe5H8JSh1qBUU", + "oNRoEyhaQvupoAt4byo46ORb+bfIvP+ZN1D3Iu2lplIf4MUVwg37cdXg3U/aDtwdezrG06LOoC/mBRRU", + "Q0YkKFHLFJQxW7ebME50DmatElxBTBSYcG/2JyZolyVVAxJ5gl0xNths2Xou5EmR1OX9sAe3VSGM7DNa", + "KBjPbqN14Ia1t5GWNH0XxRGVmikdXe9SfBzdPpqLR+6bI3FZsBRCwqsvXPo7l/rKqPOLlxrt8s7OvKJz", + "eMlKpvtiX+Vg3GoCkogZQUZIBZIYkIFLZpbeFgbdur/IYEbrQkfHfzmKo5mQJdXIjH7yOIqjknFWGumO", + "Gt4Z1zAH2bD4ejZTMMCjwDUyE8ga4xTXtjBoAQY4PIBBJWSAtUshtTeNutDKmIPgYMJxKSR4D85ABW1m", + "Qs4lzNgtoljPZr979B1Ka+gBz0yRImQGcjIgNPK3ze+t4sjbL9raU5pdwPsaFIplyhbg+COtqoKlqODp", + "gmcTWrEf/1ACo3OL/v8lzKLj6P+mbWE2tatq+kxKIV8ay0Wy6yp7SjPiCa/i6JSb2EKLS5ALkAj5afnx", + "DBDLAbEsrOLoTOiTWudCsj8h+7Q8nQlNOrQtM89FzT8DH5YsXgELYjA2gZMWxetZdPx2O5EL51RfJ39A", + "ao79Q9TeDESjtWRJre1v21Ah5ZN2O5p1gRpQOatswrKOG52oWnO0uykg0EUHcz/DiW1s3hMzBqDtiFGm", + "9zWTxu7eNgI09Np4IJxCV70v16s4GpKkp6KM6p2G0jvDqITdUJb6K9D0dctqVzhEElsO+nJ5GdbMI3S8", + "4ahh10xkMw7WR9VeLM2YSrXQoQJsPT6ajcpnAxvommARR3PgEsK4cGknQxIKoAr+QfUAGreBZFTvxqaZ", + "Lgbw4NJuBMbsRinIGigR2zW0YQKWv0C2s2EF/ZtzJ4a8wc2wJTaG9umdHpLez+s12X1YQ+MclqVrUO3l", + "sQZ12DrMj+Zqfz86wNVqrDcdUMZX5U03DanHfMJE/5KfEJWbTDdhYi5plS8bl2HvRMBn2LQ06C1oCbvg", + "N8RCZMPydBTSk0cK6/t2q/XC7NykjODDlC/EkGc1gEZKyn0UYpxQ6yBNeeB940ahG9uLezJc+A1Z/x2b", + "4afQ4p7G26aqPf7ALI1PxBBTK/NWr+FQDzI0ZHgZaMqKQCUWRywLflaa6loFl5oQ3r8nPa5Obfmf+bNF", + "Zpixz9LUzba+KmlVGQw+UmxNWdvYvPXofYI6tAtt1nDoK90zW7qiBKs4EhxGhFWPZUSqvXuX5Rvd+3nT", + "VnjJeChGzZi0pte0DmrJQq6voOP2cbgdta+SsNi9L2QKrUyvgtc6raUErs/pfMCLuQ3YDvJ5byqKAlLX", + "f+m1UfrZMCaOp/5aBnJQs95rQn0MMSPPSGJGsAOJhRS+4U97Crc3v89Wzdn7GgjLgGs2YyCx9+MaS/YW", + "D/YAx3n4K7xkG76NZR7t9RZhrhyZgDaXVRPKO4wOtDG9ewhFNdt8OeUzEUhla51fNFxv8vEmB52DVVat", + "QJKcKkLTFJ+GBH5WiHyCHVpaVsaTallDw0YiRAGUGz5mQHUtQ9ZzQgpXSwr8RAviN3eIEFVXlZBaYZ8u", + "2NdVjUbwGaNEY93Z4jXxASnsTqo8J2Kmb6iENbkjThcsk6IMGpSF/CdIxQQPE1rYxW7NF9ZwdDT529Hk", + "aGdu56TaJB6vH3vnZEKmaoPC5yjNkPKOymwn/MUaxED9YSl93d2cTW2Fy9YRdYPZhj/ZhDqBQvC5ufAh", + "s8bd2/pElWQllctQvyhIZBKksoVAv+YhN7kgFUgTZiBrqYQwJ0zLwVaQW2yw1xkTZMYKjNfvWCESpu2b", + "joJU8Ix8/y6p1A+TYPhMqgHlJ0AdlpLxWgP5/un5qx88UeQ8jDHNKedQqF2dI8u33+0j8jbEoiz9ONiG", + "p+ZLQrOMOS/tN5qqiwsNitBE1DqIf70nGObZrHjGb5jOsa4r60KzR7iEZhPmOaulfbgKI3ara1o1mrAH", + "p7pJyawQtFM3W3b26T0OS16yEvRgyH91+urZWtxvDW4SbmWmQmaMzx+VyVD286pWLH0qKeN/hlMgh2I3", + "74r9OcC3WQnfkWSpQYXPa2zndPDmah+XAgjwdNftqPE6A+wYkMP1OKy2JVC5vdFsdqzbpe9hTPZu8MZt", + "YthxzHEz6dAYYOfCdBxJ6w/deQ8GmouxLdI7fb2J935xGorsYx+Ggp0VBWktmV5eGiKutwdUgjyp7eSR", + "/e259yi/vLny78eYE+Nqe7a51pV9ImQuScdJV1vkuDfos05uWcvCQanj6bTJOidCWve6YWlMEVVBSuz3", + "xOXUDUJycn4ak5ucpTmhRSFuFOb6mOEnUtwoIJRnpKTcFKo6ByZJaW4DKVgiTWhfMEoo+eXy9dnxyfkp", + "IQlVkBFjsXJGU5gQY+1mpZJiwTJQBHhWCWYCx8zbuordC0fsoriKSVXQpakMFHJg0/mYUJMquFkt5Ias", + "ZYUT8nPBMCillBMJWjJYGG9kHbwJAm2QWiuInTyxm/pCokpI7ScQYvziMwtCsZxVRNVGc4qkEqg2rtTs", + "yqAA/KWRYULeWJaZsirPYAGFuTSW06RmReY0S6vKyozZP8ORmZxq1OkcUxIUvz1DC2bT/NjpGVkhCmiJ", + "s3xwW4FkwFNApTdn7Oo5pBY45LZwn/xmfEXBUuAKOqb54uxX8gI4SFqQ8zopWEpe2k1k8QSrlHWDnTOd", + "18kkFWVru52fkkIk05IqDXL68vTnZ2eXzzrxIlqz2yiOFr6iio4mjydH2OyqgNOKRcfRk4ktkyqqc7ym", + "09Yxze08jDkAtIrTzMgC+sQXkt1B+IGip90ybQeBhjpjG5vdSM6I3Wtz26P3NzP7oyGaWdG9IPyA7Wig", + "7jzyvkB7E+tMhI7XtP9LgBEQOBs0Yl9vNnB1vTE09Pjo6KAJlI957XOd3F6UddwGciHfAW+H+WLj+6Sd", + "OrIDWMaX/u5Q/E5wmIo0quj2brbx1mu1B9gsfDd5G6LN5vPIenyjvxt88PQcBJKEXhxue1zOBa3i6Cd7", + "5CE2GtOYdobJEOTJbpD18apVHP11DKHQuBhmO3VpKngc5HTBlPaEWcXet04/4P+n2Wqnl93byW6OF5uL", + "N6bjuzGFgUN+Jia0M36O56h7yraROTz2d783eNTF/Sou6shZgf6V4a5P1NYeh5n/T0c/jYJy83j3cF84", + "YTxjC5bVtHBmaK9MW84M3pSmn/0tIfmWkNx1QvJl5CHurfjBRHh3ZR9IiPfSdBzW9IP9YVeU9/2ng8L8", + "3uHdUwvFd8fu1xLg3Y34mNDpHlweWOxsFTNtH22H7K/z9P3ZjrLDw8HHedX5C6n//vs/ihhI8g6WpKSV", + "7eI0eZ47cZIx7NpQuSQSKgnKXBc+X3tT/tze5gVox8t3iswLkdACG3X2fNuJ1qHzdX9x9i01+pYa3Uuv", + "xtnXl5AjNRN+33o1nzCTcy7oYSRyXpjWt04/4P87srgr93h4aK/G36L9sjn/ZBlI5hzTX0ku19zch92s", + "8W/XDyvftFa46r76ovF333vfXhvrsnmMvRr2wWtKKzZdPI5W16v/BQAA//9EQaqoZUkAAA==", +} + +// GetSwagger returns the content of the embedded swagger specification file +// or error if failed to decode +func decodeSpec() ([]byte, error) { + zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) + if err != nil { + return nil, fmt.Errorf("error base64 decoding spec: %s", err) + } + zr, err := gzip.NewReader(bytes.NewReader(zipped)) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %s", err) + } + var buf bytes.Buffer + _, err = buf.ReadFrom(zr) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %s", err) + } + + return buf.Bytes(), nil +} + +var rawSpec = decodeSpecCached() + +// a naive cached of a decoded swagger spec +func decodeSpecCached() func() ([]byte, error) { + data, err := decodeSpec() + return func() ([]byte, error) { + return data, err + } +} + +// Constructs a synthetic filesystem for resolving external references when loading openapi specifications. +func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { + var res = make(map[string]func() ([]byte, error)) + if len(pathToFile) > 0 { + res[pathToFile] = rawSpec + } + + return res +} + +// GetSwagger returns the Swagger specification corresponding to the generated code +// in this file. The external references of Swagger specification are resolved. +// The logic of resolving external references is tightly connected to "import-mapping" feature. +// Externally referenced files must be embedded in the corresponding golang packages. +// Urls can be supported but this task was out of the scope. +func GetSwagger() (swagger *openapi3.T, err error) { + var resolvePath = PathToRawSpec("") + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { + var pathToFile = url.String() + pathToFile = path.Clean(pathToFile) + getSpec, ok := resolvePath[pathToFile] + if !ok { + err1 := fmt.Errorf("path not found: %s", pathToFile) + return nil, err1 + } + return getSpec() + } + var specData []byte + specData, err = rawSpec() + if err != nil { + return + } + swagger, err = loader.LoadFromData(specData) + if err != nil { + return + } + return +} diff --git a/server/api/openapi_types.cfg.yaml b/server/api/openapi_types.cfg.yaml new file mode 100644 index 000000000..8cf96c5c9 --- /dev/null +++ b/server/api/openapi_types.cfg.yaml @@ -0,0 +1,4 @@ +package: api +generate: + models: true +output: openapi_types.gen.go diff --git a/server/api/openapi_types.gen.go b/server/api/openapi_types.gen.go new file mode 100644 index 000000000..7ae8e491f --- /dev/null +++ b/server/api/openapi_types.gen.go @@ -0,0 +1,579 @@ +// Package api provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version v1.12.5-0.20230513000919-14548c7e7bbe DO NOT EDIT. +package api + +import ( + "encoding/json" + "errors" + + "github.com/deepmap/oapi-codegen/pkg/runtime" +) + +const ( + BearerAuthScopes = "bearerAuth.Scopes" +) + +// Defines values for ArtistRole. +const ( + ArtistRoleAlbumArtist ArtistRole = "albumArtist" + ArtistRoleArtist ArtistRole = "artist" +) + +// Defines values for ResourceType. +const ( + ResourceTypeAlbum ResourceType = "album" + ResourceTypeArtist ResourceType = "artist" + ResourceTypeTrack ResourceType = "track" +) + +// Defines values for ServerInfoFeatures. +const ( + Albums ServerInfoFeatures = "albums" + Artists ServerInfoFeatures = "artists" + Images ServerInfoFeatures = "images" +) + +// Album defines model for Album. +type Album struct { + Attributes *AlbumAttributes `json:"attributes,omitempty"` + + // Id The unique identifier for the resource + Id string `json:"id"` + Relationships *struct { + Artists []AlbumArtistRelationship `json:"artists"` + Tracks []AlbumTrackRelationship `json:"tracks"` + } `json:"relationships,omitempty"` + + // Type The type of the resource + Type ResourceType `json:"type"` +} + +// AlbumArtistRelationship defines model for AlbumArtistRelationship. +type AlbumArtistRelationship struct { + Data ResourceObject `json:"data"` + Meta ArtistMetaObject `json:"meta"` +} + +// AlbumAttributes defines model for AlbumAttributes. +type AlbumAttributes struct { + // Artist The artist of the album + Artist string `json:"artist"` + + // Disctotal The number of discs in the album + Disctotal *int `json:"disctotal,omitempty"` + + // Genre The genre of the album + Genre *string `json:"genre,omitempty"` + + // ReleaseDate The release date of the album + ReleaseDate *string `json:"releaseDate,omitempty"` + + // Title The title of the album + Title string `json:"title"` + + // Tracktotal The number of tracks on the album + Tracktotal *int `json:"tracktotal,omitempty"` +} + +// AlbumTrackRelationship defines model for AlbumTrackRelationship. +type AlbumTrackRelationship struct { + Data ResourceObject `json:"data"` +} + +// Artist defines model for Artist. +type Artist struct { + Attributes *ArtistAttributes `json:"attributes,omitempty"` + + // Id The unique identifier for the resource + Id string `json:"id"` + Relationships *struct { + Albums *struct { + Data []ArtistAlbumRelationship `json:"data"` + } `json:"albums,omitempty"` + Tracks *struct { + Data []ArtistTrackRelationship `json:"data"` + } `json:"tracks,omitempty"` + } `json:"relationships,omitempty"` + + // Type The type of the resource + Type ResourceType `json:"type"` +} + +// ArtistAlbumRelationship defines model for ArtistAlbumRelationship. +type ArtistAlbumRelationship struct { + Data ResourceObject `json:"data"` + Meta ArtistMetaObject `json:"meta"` +} + +// ArtistAttributes defines model for ArtistAttributes. +type ArtistAttributes struct { + // Bio A short biography of the artist + Bio *string `json:"bio,omitempty"` + + // Name The name of the artist + Name string `json:"name"` +} + +// ArtistMetaObject defines model for ArtistMetaObject. +type ArtistMetaObject struct { + // Role The role of an artist in a track or album + Role ArtistRole `json:"role"` +} + +// ArtistRole The role of an artist in a track or album +type ArtistRole string + +// ArtistTrackRelationship defines model for ArtistTrackRelationship. +type ArtistTrackRelationship struct { + Data ResourceObject `json:"data"` + Meta struct { + // Role The role of an artist in a track or album + Role ArtistRole `json:"role"` + } `json:"meta"` +} + +// ErrorList defines model for ErrorList. +type ErrorList struct { + Errors []ErrorObject `json:"errors"` +} + +// ErrorObject defines model for ErrorObject. +type ErrorObject struct { + Detail *string `json:"detail,omitempty"` + Id *string `json:"id,omitempty"` + Status *string `json:"status,omitempty"` + Title *string `json:"title,omitempty"` +} + +// IncludedResource defines model for IncludedResource. +type IncludedResource struct { + union json.RawMessage +} + +// PaginationLinks defines model for PaginationLinks. +type PaginationLinks struct { + First *string `json:"first,omitempty"` + Last *string `json:"last,omitempty"` + Next *string `json:"next,omitempty"` + Prev *string `json:"prev,omitempty"` +} + +// PaginationMeta defines model for PaginationMeta. +type PaginationMeta struct { + // CurrentPage The current page in the collection + CurrentPage *int32 `json:"currentPage,omitempty"` + + // TotalItems The total number of items in the collection + TotalItems *int32 `json:"totalItems,omitempty"` + + // TotalPages The total number of pages in the collection + TotalPages *int32 `json:"totalPages,omitempty"` +} + +// ResourceObject defines model for ResourceObject. +type ResourceObject struct { + // Id The unique identifier for the resource + Id string `json:"id"` + + // Type The type of the resource + Type ResourceType `json:"type"` +} + +// ResourceType The type of the resource +type ResourceType string + +// ServerInfo defines model for ServerInfo. +type ServerInfo struct { + // AuthRequired Whether the user has access to the server. + AuthRequired bool `json:"authRequired"` + + // Features A list of optional features the server supports. + Features []ServerInfoFeatures `json:"features"` + + // Server The name of the server software. + Server string `json:"server"` + + // ServerVersion The version number of the server. + ServerVersion string `json:"serverVersion"` +} + +// ServerInfoFeatures defines model for ServerInfo.Features. +type ServerInfoFeatures string + +// Track defines model for Track. +type Track struct { + Attributes *TrackAttributes `json:"attributes,omitempty"` + + // Id The unique identifier for the resource + Id string `json:"id"` + Relationships *TrackRelationships `json:"relationships,omitempty"` + + // Type The type of the resource + Type ResourceType `json:"type"` +} + +// TrackArtistRelationship defines model for TrackArtistRelationship. +type TrackArtistRelationship struct { + Data ResourceObject `json:"data"` + Meta ArtistMetaObject `json:"meta"` +} + +// TrackAttributes defines model for TrackAttributes. +type TrackAttributes struct { + // Album The name of the album the track belongs to + Album string `json:"album"` + + // Albumartist The primary artist of the album the track belongs to. + Albumartist string `json:"albumartist"` + + // Artist The name of the artist who performed the track + Artist string `json:"artist"` + + // Bitrate The bitrate of the audio file in kilobits per second (kbps). + Bitrate int `json:"bitrate"` + + // Bpm The beats per minute (BPM) of the track. + Bpm *int `json:"bpm,omitempty"` + + // Channels The number of audio channels in the track. + Channels int `json:"channels"` + + // Comments Any additional comments or notes about the track. + Comments *string `json:"comments,omitempty"` + + // Disc The disc number within a multi-disc album. + Disc *int `json:"disc,omitempty"` + + // Duration The duration of the track in seconds + Duration float32 `json:"duration"` + + // Genre The genre of the track. + Genre *string `json:"genre,omitempty"` + + // Mimetype The MIME type of the audio file. + Mimetype string `json:"mimetype"` + + // RecordingMbid The MusicBrainz identifier for the recording of the track. + RecordingMbid *string `json:"recording-mbid,omitempty"` + + // Size The size of the audio file in bytes. + Size int `json:"size"` + + // Title The title of the track + Title string `json:"title"` + + // Track The track number within the album. + Track int `json:"track"` + + // TrackMbid The MusicBrainz identifier for the track. + TrackMbid *string `json:"track-mbid,omitempty"` + + // Year The release year of the track or album. + Year *int `json:"year,omitempty"` +} + +// TrackRelationships defines model for TrackRelationships. +type TrackRelationships struct { + Albums *[]AlbumTrackRelationship `json:"albums,omitempty"` + Artists []TrackArtistRelationship `json:"artists"` +} + +// FilterContains defines model for filterContains. +type FilterContains = []string + +// FilterEndsWith defines model for filterEndsWith. +type FilterEndsWith = []string + +// FilterEquals defines model for filterEquals. +type FilterEquals = []string + +// FilterGreaterOrEqual defines model for filterGreaterOrEqual. +type FilterGreaterOrEqual = []string + +// FilterGreaterThan defines model for filterGreaterThan. +type FilterGreaterThan = []string + +// FilterLessOrEqual defines model for filterLessOrEqual. +type FilterLessOrEqual = []string + +// FilterLessThan defines model for filterLessThan. +type FilterLessThan = []string + +// FilterStartsWith defines model for filterStartsWith. +type FilterStartsWith = []string + +// Include defines model for include. +type Include = string + +// IncludeForAlbum defines model for includeForAlbum. +type IncludeForAlbum = includeSlice + +// IncludeForAlbums defines model for includeForAlbums. +type IncludeForAlbums = includeSlice + +// IncludeForTracks defines model for includeForTracks. +type IncludeForTracks = includeSlice + +// PageLimit defines model for pageLimit. +type PageLimit = int32 + +// PageOffset defines model for pageOffset. +type PageOffset = int32 + +// Sort defines model for sort. +type Sort = string + +// GetAlbumsParams defines parameters for GetAlbums. +type GetAlbumsParams struct { + // PageLimit The number of items per page + PageLimit *PageLimit `form:"page[limit],omitempty" json:"page[limit],omitempty"` + + // PageOffset The offset for pagination + PageOffset *PageOffset `form:"page[offset],omitempty" json:"page[offset],omitempty"` + + // FilterEquals Filter by any property with an exact match. Usage: filter[equals]=property:value + FilterEquals *FilterEquals `form:"filter[equals],omitempty" json:"filter[equals],omitempty"` + + // FilterContains Filter by any property containing text. Usage: filter[contains]=property:value + FilterContains *FilterContains `form:"filter[contains],omitempty" json:"filter[contains],omitempty"` + + // FilterLessThan Filter by any numeric property less than a value. Usage: filter[lessThan]=property:value + FilterLessThan *FilterLessThan `form:"filter[lessThan],omitempty" json:"filter[lessThan],omitempty"` + + // FilterLessOrEqual Filter by any numeric property less than or equal to a value. Usage: filter[lessOrEqual]=property:value + FilterLessOrEqual *FilterLessOrEqual `form:"filter[lessOrEqual],omitempty" json:"filter[lessOrEqual],omitempty"` + + // FilterGreaterThan Filter by any numeric property greater than a value. Usage: filter[greaterThan]=property:value + FilterGreaterThan *FilterGreaterThan `form:"filter[greaterThan],omitempty" json:"filter[greaterThan],omitempty"` + + // FilterGreaterOrEqual Filter by any numeric property greater than or equal to a value. Usage: filter[greaterOrEqual]=property:value + FilterGreaterOrEqual *FilterGreaterOrEqual `form:"filter[greaterOrEqual],omitempty" json:"filter[greaterOrEqual],omitempty"` + + // FilterStartsWith Filter by any property that starts with text. Usage: filter[startsWith]=property:value + FilterStartsWith *FilterStartsWith `form:"filter[startsWith],omitempty" json:"filter[startsWith],omitempty"` + + // FilterEndsWith Filter by any property that ends with text. Usage: filter[endsWith]=property:value + FilterEndsWith *FilterEndsWith `form:"filter[endsWith],omitempty" json:"filter[endsWith],omitempty"` + + // Sort Sort the results by one or more properties, separated by commas. Prefix the property with '-' for descending order. + Sort *Sort `form:"sort,omitempty" json:"sort,omitempty"` + + // Include Related resources to include in the response, separated by commas + Include *IncludeForAlbums `form:"include,omitempty" json:"include,omitempty"` +} + +// GetAlbumParams defines parameters for GetAlbum. +type GetAlbumParams struct { + // Include Related resources to include in the response, separated by commas + Include *IncludeForAlbum `form:"include,omitempty" json:"include,omitempty"` +} + +// GetArtistsParams defines parameters for GetArtists. +type GetArtistsParams struct { + // PageLimit The number of items per page + PageLimit *PageLimit `form:"page[limit],omitempty" json:"page[limit],omitempty"` + + // PageOffset The offset for pagination + PageOffset *PageOffset `form:"page[offset],omitempty" json:"page[offset],omitempty"` + + // FilterEquals Filter by any property with an exact match. Usage: filter[equals]=property:value + FilterEquals *FilterEquals `form:"filter[equals],omitempty" json:"filter[equals],omitempty"` + + // FilterContains Filter by any property containing text. Usage: filter[contains]=property:value + FilterContains *FilterContains `form:"filter[contains],omitempty" json:"filter[contains],omitempty"` + + // FilterLessThan Filter by any numeric property less than a value. Usage: filter[lessThan]=property:value + FilterLessThan *FilterLessThan `form:"filter[lessThan],omitempty" json:"filter[lessThan],omitempty"` + + // FilterLessOrEqual Filter by any numeric property less than or equal to a value. Usage: filter[lessOrEqual]=property:value + FilterLessOrEqual *FilterLessOrEqual `form:"filter[lessOrEqual],omitempty" json:"filter[lessOrEqual],omitempty"` + + // FilterGreaterThan Filter by any numeric property greater than a value. Usage: filter[greaterThan]=property:value + FilterGreaterThan *FilterGreaterThan `form:"filter[greaterThan],omitempty" json:"filter[greaterThan],omitempty"` + + // FilterGreaterOrEqual Filter by any numeric property greater than or equal to a value. Usage: filter[greaterOrEqual]=property:value + FilterGreaterOrEqual *FilterGreaterOrEqual `form:"filter[greaterOrEqual],omitempty" json:"filter[greaterOrEqual],omitempty"` + + // FilterStartsWith Filter by any property that starts with text. Usage: filter[startsWith]=property:value + FilterStartsWith *FilterStartsWith `form:"filter[startsWith],omitempty" json:"filter[startsWith],omitempty"` + + // FilterEndsWith Filter by any property that ends with text. Usage: filter[endsWith]=property:value + FilterEndsWith *FilterEndsWith `form:"filter[endsWith],omitempty" json:"filter[endsWith],omitempty"` + + // Sort Sort the results by one or more properties, separated by commas. Prefix the property with '-' for descending order. + Sort *Sort `form:"sort,omitempty" json:"sort,omitempty"` + + // Include Related resources to include in the response, separated by commas + Include *Include `form:"include,omitempty" json:"include,omitempty"` +} + +// GetArtistParams defines parameters for GetArtist. +type GetArtistParams struct { + // Include Related resources to include in the response, separated by commas + Include *Include `form:"include,omitempty" json:"include,omitempty"` +} + +// GetTracksParams defines parameters for GetTracks. +type GetTracksParams struct { + // PageLimit The number of items per page + PageLimit *PageLimit `form:"page[limit],omitempty" json:"page[limit],omitempty"` + + // PageOffset The offset for pagination + PageOffset *PageOffset `form:"page[offset],omitempty" json:"page[offset],omitempty"` + + // FilterEquals Filter by any property with an exact match. Usage: filter[equals]=property:value + FilterEquals *FilterEquals `form:"filter[equals],omitempty" json:"filter[equals],omitempty"` + + // FilterContains Filter by any property containing text. Usage: filter[contains]=property:value + FilterContains *FilterContains `form:"filter[contains],omitempty" json:"filter[contains],omitempty"` + + // FilterLessThan Filter by any numeric property less than a value. Usage: filter[lessThan]=property:value + FilterLessThan *FilterLessThan `form:"filter[lessThan],omitempty" json:"filter[lessThan],omitempty"` + + // FilterLessOrEqual Filter by any numeric property less than or equal to a value. Usage: filter[lessOrEqual]=property:value + FilterLessOrEqual *FilterLessOrEqual `form:"filter[lessOrEqual],omitempty" json:"filter[lessOrEqual],omitempty"` + + // FilterGreaterThan Filter by any numeric property greater than a value. Usage: filter[greaterThan]=property:value + FilterGreaterThan *FilterGreaterThan `form:"filter[greaterThan],omitempty" json:"filter[greaterThan],omitempty"` + + // FilterGreaterOrEqual Filter by any numeric property greater than or equal to a value. Usage: filter[greaterOrEqual]=property:value + FilterGreaterOrEqual *FilterGreaterOrEqual `form:"filter[greaterOrEqual],omitempty" json:"filter[greaterOrEqual],omitempty"` + + // FilterStartsWith Filter by any property that starts with text. Usage: filter[startsWith]=property:value + FilterStartsWith *FilterStartsWith `form:"filter[startsWith],omitempty" json:"filter[startsWith],omitempty"` + + // FilterEndsWith Filter by any property that ends with text. Usage: filter[endsWith]=property:value + FilterEndsWith *FilterEndsWith `form:"filter[endsWith],omitempty" json:"filter[endsWith],omitempty"` + + // Sort Sort the results by one or more properties, separated by commas. Prefix the property with '-' for descending order. + Sort *Sort `form:"sort,omitempty" json:"sort,omitempty"` + + // Include Related resources to include in the response, separated by commas + Include *IncludeForTracks `form:"include,omitempty" json:"include,omitempty"` +} + +// GetTrackParams defines parameters for GetTrack. +type GetTrackParams struct { + // Include Related resources to include in the response, separated by commas + Include *IncludeForTracks `form:"include,omitempty" json:"include,omitempty"` +} + +// AsTrack returns the union data inside the IncludedResource as a Track +func (t IncludedResource) AsTrack() (Track, error) { + var body Track + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromTrack overwrites any union data inside the IncludedResource as the provided Track +func (t *IncludedResource) FromTrack(v Track) error { + v.Type = "track" + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeTrack performs a merge with any union data inside the IncludedResource, using the provided Track +func (t *IncludedResource) MergeTrack(v Track) error { + v.Type = "track" + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JsonMerge(t.union, b) + t.union = merged + return err +} + +// AsAlbum returns the union data inside the IncludedResource as a Album +func (t IncludedResource) AsAlbum() (Album, error) { + var body Album + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromAlbum overwrites any union data inside the IncludedResource as the provided Album +func (t *IncludedResource) FromAlbum(v Album) error { + v.Type = "album" + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeAlbum performs a merge with any union data inside the IncludedResource, using the provided Album +func (t *IncludedResource) MergeAlbum(v Album) error { + v.Type = "album" + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JsonMerge(t.union, b) + t.union = merged + return err +} + +// AsArtist returns the union data inside the IncludedResource as a Artist +func (t IncludedResource) AsArtist() (Artist, error) { + var body Artist + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromArtist overwrites any union data inside the IncludedResource as the provided Artist +func (t *IncludedResource) FromArtist(v Artist) error { + v.Type = "artist" + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeArtist performs a merge with any union data inside the IncludedResource, using the provided Artist +func (t *IncludedResource) MergeArtist(v Artist) error { + v.Type = "artist" + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JsonMerge(t.union, b) + t.union = merged + return err +} + +func (t IncludedResource) Discriminator() (string, error) { + var discriminator struct { + Discriminator string `json:"type"` + } + err := json.Unmarshal(t.union, &discriminator) + return discriminator.Discriminator, err +} + +func (t IncludedResource) ValueByDiscriminator() (interface{}, error) { + discriminator, err := t.Discriminator() + if err != nil { + return nil, err + } + switch discriminator { + case "album": + return t.AsAlbum() + case "artist": + return t.AsArtist() + case "track": + return t.AsTrack() + default: + return nil, errors.New("unknown discriminator value: " + discriminator) + } +} + +func (t IncludedResource) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *IncludedResource) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} diff --git a/server/auth.go b/server/auth.go index cc59fd195..e36324168 100644 --- a/server/auth.go +++ b/server/auth.go @@ -174,7 +174,9 @@ func validateLogin(userRepo model.UserRepository, userName, password string) (*m func authHeaderMapper(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { bearer := r.Header.Get(consts.UIAuthorizationHeader) - r.Header.Set("Authorization", bearer) + if bearer != "" { + r.Header.Set("Authorization", bearer) + } next.ServeHTTP(w, r) }) } diff --git a/server/auth_test.go b/server/auth_test.go index dbaa79e12..a23c12a1c 100644 --- a/server/auth_test.go +++ b/server/auth_test.go @@ -176,6 +176,18 @@ var _ = Describe("Auth", func() { w.WriteHeader(200) })).ServeHTTP(w, r) + Expect(w.Code).To(Equal(200)) + }) + It("does not override the Authorization header if x-nd-authorizer is not present", func() { + r := httptest.NewRequest("GET", "/index.html", nil) + r.Header.Set("Authorization", "original bearer") + w := httptest.NewRecorder() + + authHeaderMapper(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Expect(r.Header.Get("Authorization")).To(Equal("original bearer")) + w.WriteHeader(200) + })).ServeHTTP(w, r) + Expect(w.Code).To(Equal(200)) }) }) diff --git a/server/middlewares.go b/server/middlewares.go index cdbefacb8..8591d3de0 100644 --- a/server/middlewares.go +++ b/server/middlewares.go @@ -190,41 +190,31 @@ var ( // handling the given request, as determined by the presence of X-Forwarded-* headers // or the scheme and host of the request URL. func serverAddress(r *http.Request) (scheme, host string) { - // Save the original request host for later comparison. - origHost := r.Host - // Determine the protocol of the request based on the presence of a TLS connection. protocol := "http" if r.TLS != nil { protocol = "https" } - // Get the X-Forwarded-Host header and extract the first host name if there are - // multiple hosts listed. If there is no X-Forwarded-Host header, use the original - // request host as the default. - xfh := r.Header.Get(xForwardedHost) - if xfh != "" { - i := strings.Index(xfh, ",") - if i == -1 { - i = len(xfh) - } - xfh = xfh[:i] - } - host = FirstOr(r.Host, xfh) - // Determine the protocol and scheme of the request based on the presence of // X-Forwarded-* headers or the scheme of the request URL. - scheme = FirstOr( - protocol, + scheme = Coalesce( r.Header.Get(xForwardedProto), r.Header.Get(xForwardedScheme), r.URL.Scheme, + protocol, ) + // Get the X-Forwarded-Host header and extract the first host name if there are + // multiple hosts listed. If there is no X-Forwarded-Host header, use the original + // request host as the default. + parts := strings.Split(r.Header.Get(xForwardedHost), ",") + host = Coalesce(parts[0], r.Host) + // If the request host has changed due to the X-Forwarded-Host header, log a trace // message with the original and new host values, as well as the scheme and URL. - if host != origHost { - log.Trace(r.Context(), "Request host has changed", "origHost", origHost, "host", host, "scheme", scheme, "url", r.URL) + if host != r.Host { + log.Trace(r.Context(), "Request host has changed", "origHost", r.Host, "host", host, "scheme", scheme, "url", r.URL) } // Return the scheme and host of the server handling the request. diff --git a/server/middlewares_test.go b/server/middlewares_test.go index 7823b1ef8..28ebe0971 100644 --- a/server/middlewares_test.go +++ b/server/middlewares_test.go @@ -86,7 +86,7 @@ var _ = Describe("middlewares", func() { }) }) - Context("with X-Forwarded-Host header", func() { + Context("with X-Forwarded-Host header with one host", func() { BeforeEach(func() { req, _ = http.NewRequest("GET", "http://example.com", nil) req.Header.Set("X-Forwarded-Host", "forwarded.example.com") @@ -99,6 +99,19 @@ var _ = Describe("middlewares", func() { }) }) + Context("with X-Forwarded-Host header with multiple hosts", func() { + BeforeEach(func() { + req, _ = http.NewRequest("GET", "http://example.com", nil) + req.Header.Set("X-Forwarded-Host", "forwarded.example.com,forwarded2.example.com") + }) + + It("should modify the request with the X-Forwarded-Host header value", func() { + middleware.ServeHTTP(recorder, req) + Expect(req.Host).To(Equal("forwarded.example.com")) + Expect(req.URL.Scheme).To(Equal("http")) + }) + }) + Context("with X-Forwarded-Proto header", func() { BeforeEach(func() { req, _ = http.NewRequest("GET", "http://example.com", nil) diff --git a/server/server_test.go b/server/server_test.go index 61d2f78da..26155f3c0 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -5,11 +5,15 @@ import ( "net/url" "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/conf/configtest" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("AbsoluteURL", func() { + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig()) + }) When("BaseURL is empty", func() { BeforeEach(func() { conf.Server.BasePath = "" diff --git a/utils/gg/gg.go b/utils/gg/gg.go index 6da9082a1..95b2aa1e3 100644 --- a/utils/gg/gg.go +++ b/utils/gg/gg.go @@ -13,20 +13,37 @@ func If[T comparable](v T, orElse T) T { return orElse } -// FirstOr is a generic helper function that returns the first non-zero value from -// a list of comparable values, or a default value if all the values are zero. -func FirstOr[T comparable](or T, values ...T) T { - // Initialize a zero value of the same type as the input values. +// P returns a pointer to a value, or nil if the value is the zero value of the type. +func P[T comparable](t T) *T { var zero T + if t == zero { + return nil + } + return &t +} - // Loop through each input value and check if it is non-zero. If a non-zero value - // is found, return it immediately. - for _, v := range values { +// V returns the value of a pointer, or the zero value of the type if the pointer is nil. +func V[T comparable](p *T) T { + var zero T + if p == nil { + return zero + } + return *p +} + +// Coalesce returns the first non-zero value from listed arguments. +// Returns the zero value of the type parameter if no arguments are given or all are the zero value. +// Useful when you want to initialize a variable to the first non-zero value from a list of fallback values. +// +// For example: +// +// hostVal := Coalesce(hostName, os.Getenv("HOST"), "localhost") +func Coalesce[T comparable](values ...T) (v T) { + var zero T + for _, v = range values { if v != zero { - return v + return } } - - // If all the input values are zero, return the default value. - return or + return } diff --git a/utils/gg/gg_test.go b/utils/gg/gg_test.go index e492fae9d..c82079b69 100644 --- a/utils/gg/gg_test.go +++ b/utils/gg/gg_test.go @@ -43,16 +43,54 @@ var _ = Describe("GG", func() { ) }) - Describe("FirstOr", func() { + Describe("Coalesce", func() { Context("when given a list of strings", func() { It("returns the first non-empty value", func() { - Expect(gg.FirstOr("default", "foo", "bar", "baz")).To(Equal("foo")) - Expect(gg.FirstOr("default", "", "", "qux")).To(Equal("qux")) + Expect(gg.Coalesce("foo", "bar", "baz", "default")).To(Equal("foo")) + Expect(gg.Coalesce("", "", "qux", "default")).To(Equal("qux")) }) It("returns the default value if all values are empty", func() { - Expect(gg.FirstOr("default", "", "", "")).To(Equal("default")) - Expect(gg.FirstOr("", "", "", "")).To(Equal("")) + Expect(gg.Coalesce("", "", "", "default")).To(Equal("default")) + Expect(gg.Coalesce("", "", "", "")).To(Equal("")) + }) + }) + }) + + Describe("P", func() { + Context("when given a non-zero value", func() { + It("should return a non-nil pointer to that value", func() { + value := 42 + result := gg.P(value) + Expect(result).ToNot(BeNil()) + Expect(*result).To(Equal(value)) + }) + }) + + Context("when given the zero value of a type", func() { + It("should return nil", func() { + var value string + result := gg.P(value) + Expect(result).To(BeNil()) + }) + }) + }) + + Describe("V", func() { + Context("when given a non-nil pointer", func() { + It("should return the value of that pointer", func() { + value := 42 + pointer := &value + result := gg.V(pointer) + Expect(result).To(Equal(value)) + }) + }) + + Context("when given a nil pointer", func() { + It("should return the zero value of the type", func() { + var pointer *string + result := gg.V(pointer) + Expect(result).To(Equal("")) }) }) }) diff --git a/utils/strings.go b/utils/strings.go index 9fbe0d448..2753aeacf 100644 --- a/utils/strings.go +++ b/utils/strings.go @@ -57,3 +57,20 @@ func LongestCommonPrefix(list []string) string { } return list[0] } + +func SplitFunc(delimiter rune) func(c rune) bool { + open := 0 + return func(c rune) bool { + if c == '(' { + open++ + return false + } + if open > 0 { + if c == ')' { + open-- + } + return false + } + return c == delimiter + } +} diff --git a/utils/strings_test.go b/utils/strings_test.go index 0983faa29..706ae6843 100644 --- a/utils/strings_test.go +++ b/utils/strings_test.go @@ -1,6 +1,8 @@ package utils import ( + "strings" + "github.com/navidrome/navidrome/conf" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -57,123 +59,35 @@ var _ = Describe("Strings", func() { }) Describe("LongestCommonPrefix", func() { + var testPaths = []string{ + "/Music/iTunes 1/iTunes Media/Music/ABBA/Gold_ Greatest Hits/Dancing Queen.m4a", + "/Music/iTunes 1/iTunes Media/Music/ABBA/Gold_ Greatest Hits/Mamma Mia.m4a", + "/Music/iTunes 1/iTunes Media/Music/Bachman-Turner Overdrive/Gold/Down Down.m4a", + "/Music/iTunes 1/iTunes Media/Music/Bachman-Turner Overdrive/Gold/Hey You.m4a", + "/Music/iTunes 1/iTunes Media/Music/Bachman-Turner Overdrive/Gold/Hold Back The Water.m4a", + "/Music/iTunes 1/iTunes Media/Music/Compilations/Saturday Night Fever/01 Stayin' Alive.m4a", + "/Music/iTunes 1/iTunes Media/Music/Compilations/Saturday Night Fever/03 Night Fever.m4a", + "/Music/iTunes 1/iTunes Media/Music/Yes/Fragile/01 Roundabout.m4a", + } + It("finds the longest common prefix", func() { Expect(LongestCommonPrefix(testPaths)).To(Equal("/Music/iTunes 1/iTunes Media/Music/")) }) }) + Describe("SplitFunc", func() { + DescribeTable("when splitting strings with a delimiter", + func(delimiter rune, input string, expected []string) { + splitFunc := SplitFunc(delimiter) + actual := strings.FieldsFunc(input, splitFunc) + Expect(actual).To(Equal(expected)) + }, + Entry("should split strings without parentheses", ',', "name,age,email", []string{"name", "age", "email"}), + Entry("should not split strings within parentheses", ',', "name, substr(email, 0, 3), age", []string{"name", " substr(email, 0, 3)", " age"}), + Entry("should handle multiple delimiters outside parentheses", ';', "name;age;email", []string{"name", "age", "email"}), + Entry("should return the whole input as a single element if the delimiter is not found", ';', "name,age,email", []string{"name,age,email"}), + Entry("should handle empty input", ',', "", []string{}), + Entry("should handle input with only delimiters", ',', ",,,", []string{}), + ) + }) }) - -var testPaths = []string{ - "/Music/iTunes 1/iTunes Media/Music/ABBA/Gold_ Greatest Hits/Dancing Queen.m4a", - "/Music/iTunes 1/iTunes Media/Music/ABBA/Gold_ Greatest Hits/Mamma Mia.m4a", - "/Music/iTunes 1/iTunes Media/Music/Art Blakey/A Night At Birdland, Vol. 1/01 Annoucement By Pee Wee Marquette.m4a", - "/Music/iTunes 1/iTunes Media/Music/Art Blakey/A Night At Birdland, Vol. 1/02 Split Kick.m4a", - "/Music/iTunes 1/iTunes Media/Music/As Frenéticas/As Frenéticas/Perigosa.m4a", - "/Music/iTunes 1/iTunes Media/Music/Bachman-Turner Overdrive/Gold/Down Down.m4a", - "/Music/iTunes 1/iTunes Media/Music/Bachman-Turner Overdrive/Gold/Hey You.m4a", - "/Music/iTunes 1/iTunes Media/Music/Bachman-Turner Overdrive/Gold/Hold Back The Water.m4a", - "/Music/iTunes 1/iTunes Media/Music/Belle And Sebastian/Write About Love/01 I Didn't See It Coming.m4a", - "/Music/iTunes 1/iTunes Media/Music/Belle And Sebastian/Write About Love/02 Come On Sister.m4a", - "/Music/iTunes 1/iTunes Media/Music/Black Eyed Peas/Elephunk/03 Let's Get Retarded.m4a", - "/Music/iTunes 1/iTunes Media/Music/Black Eyed Peas/Elephunk/04 Hey Mama.m4a", - "/Music/iTunes 1/iTunes Media/Music/Black Eyed Peas/Monkey Business/10 They Don't Want Music (Feat. James Brown).m4a", - "/Music/iTunes 1/iTunes Media/Music/Black Eyed Peas/The E.N.D/1-01 Boom Boom Pow.m4a", - "/Music/iTunes 1/iTunes Media/Music/Black Eyed Peas/Timeless/01 Mas Que Nada.m4a", - "/Music/iTunes 1/iTunes Media/Music/Blondie/Heart Of Glass/Heart Of Glass.m4a", - "/Music/iTunes 1/iTunes Media/Music/Bob Dylan/Nashville Skyline/06 Lay Lady Lay.m4a", - "/Music/iTunes 1/iTunes Media/Music/Botany/Feeling Today - EP/03 Waterparker.m4a", - "/Music/iTunes 1/iTunes Media/Music/Céu/CéU/06 10 Contados.m4a", - "/Music/iTunes 1/iTunes Media/Music/Chance/Six Through Ten/03 Forgive+Forget.m4a", - "/Music/iTunes 1/iTunes Media/Music/Clive Tanaka Y Su Orquesta/Jet Set Siempre 1°/03 Neu Chicago (Side A) [For Dance].m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Absolute Rock Classics/1-02 Smoke on the water.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Almost Famous Soundtrack/10 Simple Man.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Audio News - Rock'n' Roll Forever/01 Rock Around The Clock.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Austin Powers_ International Man Of Mystery/01 The Magic Piper (Of Love).m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Austin Powers_ The Spy Who Shagged Me/04 American Woman.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Back To Dance/03 Long Cool Woman In A Black Dress.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Back To The 70's - O Album Da Década/03 American Pie.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Bambolê/09 In The Mood.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Bambolê - Volume II/03 Blue Moon.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Big Brother Brasil 2004/04 I Will Survive.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Collateral Soundtrack/03 Hands Of Time.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Forrest Gump - The Soundtrack/1-12 California Dreamin'.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Forrest Gump - The Soundtrack/1-16 Mrs. Robinson.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Ghost World - Original Motion Picture Soundtrack/01 Jaan Pechechaan Ho.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Grease [Original Soundtrack]/01 Grease.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/La Bamba/09 Summertime Blues.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Pretty Woman/10 Oh Pretty Woman.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents African Groove/01 Saye Mogo Bana.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents Arabic Groove/02 Galbi.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents Asian Groove/03 Remember Tomorrow.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents Blues Lounge/01 Midnight Dream.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents Blues Lounge/03 Banal Reality.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents Blues Lounge/04 Parchman Blues.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents Blues Lounge/06 Run On.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents Brazilian Groove/01 Maria Moita.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents Brazilian Lounge/08 E Depois....m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents Brazilian Lounge/11 Os Grilos.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents Euro Lounge/01 Un Simple Histoire.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents Euro Lounge/02 Limbe.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents Euro Lounge/05 Sempre Di Domenica.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents Euro Lounge/12 Voulez-Vous_.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents World Lounge/03 Santa Maria.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents_ A New Groove/02 Dirty Laundry.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents_ Blues Around the World/02 Canceriano Sem Lar (Clinica Tobias Blues).m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents_ Euro Groove/03 Check In.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Putumayo Presents_ World Groove/01 Attention.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Saturday Night Fever/01 Stayin' Alive.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/Saturday Night Fever/03 Night Fever.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/The Best Air Guitar Album In The World... Ever!/2-06 Johnny B. Goode.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/The Full Monty - Soundtrack/02 You Sexy Thing.m4a", - "/Music/iTunes 1/iTunes Media/Music/Compilations/The Full Monty - Soundtrack/11 We Are Family.m4a", - "/Music/iTunes 1/iTunes Media/Music/Cut Copy/Zonoscope (Bonus Version)/10 Corner of the Sky.m4a", - "/Music/iTunes 1/iTunes Media/Music/David Bowie/Changesbowie/07 Diamond Dogs.m4a", - "/Music/iTunes 1/iTunes Media/Music/Douster & Savage Skulls/Get Rich or High Tryin' - EP/01 Bad Gal.m4a", - "/Music/iTunes 1/iTunes Media/Music/Elton John/Greatest Hits 1970-2002/1-04 Rocket Man (I Think It's Going to Be a Long, Long Time).m4a", - "/Music/iTunes 1/iTunes Media/Music/Elvis Presley/ELV1S 30 #1 Hits/02 Don't Be Cruel.m4a", - "/Music/iTunes 1/iTunes Media/Music/Eric Clapton/The Cream Of Clapton/03 I Feel Free.m4a", - "/Music/iTunes 1/iTunes Media/Music/Fleetwood Mac/The Very Best Of Fleetwood Mac/02 Don't Stop.m4a", - "/Music/iTunes 1/iTunes Media/Music/Françoise Hardy/Comment te dire adieu/Comment te dire adieu.m4a", - "/Music/iTunes 1/iTunes Media/Music/Games/That We Can Play - EP/01 Strawberry Skies.m4a", - "/Music/iTunes 1/iTunes Media/Music/Grand Funk Railroad/Collectors Series/The Loco-Motion.m4a", - "/Music/iTunes 1/iTunes Media/Music/Henry Mancini/The Pink Panther (Music from the Film Score)/The Pink Panther Theme.m4a", - "/Music/iTunes 1/iTunes Media/Music/Holy Ghost!/Do It Again - Single/01 Do It Again.m4a", - "/Music/iTunes 1/iTunes Media/Music/K.C. & The Sunshine Band/The Best of/03 I'm Your Boogie Man.m4a", - "/Music/iTunes 1/iTunes Media/Music/K.C. & The Sunshine Band/Unknown Album/Megamix (Thats The Way, Shake Your Booty, Get Down Tonight, Give It Up).m4a", - "/Music/iTunes 1/iTunes Media/Music/Kim Ann Foxman & Andy Butler/Creature - EP/01 Creature.m4a", - "/Music/iTunes 1/iTunes Media/Music/Nico/Chelsea Girl/01 The Fairest Of The Seasons.m4a", - "/Music/iTunes 1/iTunes Media/Music/oOoOO/oOoOO - EP/02 Burnout Eyess.m4a", - "/Music/iTunes 1/iTunes Media/Music/Peter Frampton/The Very Best of Peter Frampton/Baby, I Love Your Way.m4a", - "/Music/iTunes 1/iTunes Media/Music/Peter Frampton/The Very Best of Peter Frampton/Show Me The Way.m4a", - "/Music/iTunes 1/iTunes Media/Music/Raul Seixas/A Arte De Raul Seixas/03 Metamorfose Ambulante.m4a", - "/Music/iTunes 1/iTunes Media/Music/Raul Seixas/A Arte De Raul Seixas/18 Eu Nasci há 10 Mil Anos Atrás.m4a", - "/Music/iTunes 1/iTunes Media/Music/Rick James/Street Songs/Super Freak.m4a", - "/Music/iTunes 1/iTunes Media/Music/Rita Lee/Fruto Proibido/Agora Só Falta Você.m4a", - "/Music/iTunes 1/iTunes Media/Music/Rita Lee/Fruto Proibido/Esse Tal De Roque Enrow.m4a", - "/Music/iTunes 1/iTunes Media/Music/Roberto Carlos/Roberto Carlos 1966/05 Negro Gato.m4a", - "/Music/iTunes 1/iTunes Media/Music/SOHO/Goddess/02 Hippychick.m4a", - "/Music/iTunes 1/iTunes Media/Music/Stan Getz/Getz_Gilberto/05 Corcovado (Quiet Nights of Quiet Stars).m4a", - "/Music/iTunes 1/iTunes Media/Music/Steely Dan/Pretzel Logic/Rikki Don't Loose That Number.m4a", - "/Music/iTunes 1/iTunes Media/Music/Stevie Wonder/For Once In My Life/I Don't Know Why.m4a", - "/Music/iTunes 1/iTunes Media/Music/Teebs/Ardour/While You Doooo.m4a", - "/Music/iTunes 1/iTunes Media/Music/The Beatles/Magical Mystery Tour/08 Strawberry Fields Forever.m4a", - "/Music/iTunes 1/iTunes Media/Music/The Beatles/Past Masters, Vol. 1/10 Long Tall Sally.m4a", - "/Music/iTunes 1/iTunes Media/Music/The Beatles/Please Please Me/14 Twist And Shout.m4a", - "/Music/iTunes 1/iTunes Media/Music/The Beatles/Sgt. Pepper's Lonely Hearts Club Band/03 Lucy In The Sky With Diamonds.m4a", - "/Music/iTunes 1/iTunes Media/Music/The Black Crowes/Amorica/09 Wiser Time.m4a", - "/Music/iTunes 1/iTunes Media/Music/The Black Crowes/By Your Side/05 Only A Fool.m4a", - "/Music/iTunes 1/iTunes Media/Music/The Black Crowes/Shake Your Money Maker/04 Could I''ve Been So Blind.m4a", - "/Music/iTunes 1/iTunes Media/Music/The Black Crowes/The Southern Harmony And Musical Companion/01 Sting Me.m4a", - "/Music/iTunes 1/iTunes Media/Music/The Black Crowes/Three Snakes And One Charm/02 Good Friday.m4a", - "/Music/iTunes 1/iTunes Media/Music/The Doors/Strange Days (40th Anniversary Mixes)/01 Strange Days.m4a", - "/Music/iTunes 1/iTunes Media/Music/The Rolling Stones/Forty Licks/1-03 (I Can't Get No) Satisfaction.m4a", - "/Music/iTunes 1/iTunes Media/Music/The Velvet Underground/The Velvet Underground & Nico/02 I'm Waiting For The Man.m4a", - "/Music/iTunes 1/iTunes Media/Music/The Velvet Underground/The Velvet Underground & Nico/03 Femme Fatale.m4a", - "/Music/iTunes 1/iTunes Media/Music/The Velvet Underground/White Light_White Heat/04 Here She Comes Now.m4a", - "/Music/iTunes 1/iTunes Media/Music/The Who/Sings My Generation/My Generation.m4a", - "/Music/iTunes 1/iTunes Media/Music/Village People/The Very Best Of Village People/Macho Man.m4a", - "/Music/iTunes 1/iTunes Media/Music/Vondelpark/Sauna - EP/01 California Analog Dream.m4a", - "/Music/iTunes 1/iTunes Media/Music/War/Why Can't We Be Friends/Low Rider.m4a", - "/Music/iTunes 1/iTunes Media/Music/Yes/Fragile/01 Roundabout.m4a", -}