feat: MVP's out 🚀
This commit is contained in:
commit
75394b92a0
44
.air.toml
Normal file
44
.air.toml
Normal file
|
@ -0,0 +1,44 @@
|
|||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ./cmd/handler/..."
|
||||
delay = 0
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
include_file = []
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = false
|
||||
stop_on_error = false
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
keep_scroll = true
|
24
.dockerignore
Normal file
24
.dockerignore
Normal file
|
@ -0,0 +1,24 @@
|
|||
.dockerignore
|
||||
.editorconfig
|
||||
.git
|
||||
.github
|
||||
.gitignore
|
||||
.vscode
|
||||
*.dsl
|
||||
*.env
|
||||
*.env.local
|
||||
*.json
|
||||
*.md
|
||||
*.puml
|
||||
*.sql
|
||||
*.toml
|
||||
*.yaml
|
||||
*.yml
|
||||
bin
|
||||
Dockerfile
|
||||
internal/**/*.graphql
|
||||
internal/**/genqlient.yaml
|
||||
LICENSE
|
||||
Makefile
|
||||
src
|
||||
tmp
|
28
.editorconfig
Normal file
28
.editorconfig
Normal file
|
@ -0,0 +1,28 @@
|
|||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[.vscode/*.json]
|
||||
insert_final_newline = false
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.mk]
|
||||
indent_style = tab
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
|
||||
[go.mod]
|
||||
indent_style = tab
|
||||
|
||||
[.git*]
|
||||
indent_style = tab
|
33
.github/ISSUE_TEMPLATE/BUG.md
vendored
Normal file
33
.github/ISSUE_TEMPLATE/BUG.md
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Device information:**
|
||||
|
||||
- OS: [e.g. iOS]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
22
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always
|
||||
frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've
|
||||
considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
21
.github/PULL_REQUEST_TEMPLATE/SEMANTIC.md
vendored
Normal file
21
.github/PULL_REQUEST_TEMPLATE/SEMANTIC.md
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Changes
|
||||
|
||||
## Non-breaking
|
||||
|
||||
- [ ] `feat` - adds functionality for the user
|
||||
- [ ] `fix` - fixes an issue for the user
|
||||
- [ ] `docs` - changes to the documentation
|
||||
- [ ] `style` - formatting, missing semi colons, etc
|
||||
- [ ] `refactor` - refactoring production code, eg. renaming a variable
|
||||
- [ ] `test` - adding missing tests, refactoring tests
|
||||
- [ ] `chore` - configuration of lint, IDE, issue template, etc
|
||||
|
||||
## Breaking
|
||||
|
||||
- [ ] `feat!` - adds functionality for the user, but changes existing functionality
|
||||
- [ ] `fix!` - fixes an issue for the user, but changes existing functionality
|
||||
- [ ] `chore!` - updating yarn/lerna scripts, etc
|
||||
|
||||
## Changelog
|
||||
|
||||
{{changelog}}
|
14
.github/dependabot.yml
vendored
Normal file
14
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- dependencies
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- dependencies
|
30
.github/workflows/codeql.yml
vendored
Normal file
30
.github/workflows/codeql.yml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
name: "CodeQL"
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 9 * * 1'
|
||||
|
||||
env:
|
||||
GOLANG_VERSION: 1.16
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: setup golang
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.GOLANG_VERSION }}
|
||||
- name: initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: go
|
||||
- name: vendor
|
||||
run: go mod vendor
|
||||
- name: build
|
||||
uses: wwmoraes/actions/golang/build@master
|
||||
- name: CodeQL analysis
|
||||
uses: github/codeql-action/analyze@v1
|
440
.github/workflows/integration.yml
vendored
Normal file
440
.github/workflows/integration.yml
vendored
Normal file
|
@ -0,0 +1,440 @@
|
|||
name: CI
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- '*'
|
||||
paths:
|
||||
- .github/workflows/integration.yml
|
||||
- .golangci.yaml
|
||||
- .goreleaser.yaml
|
||||
- container-structure-test.yaml
|
||||
- Dockerfile
|
||||
- .dockerignore
|
||||
- go.mod
|
||||
- go.sum
|
||||
- '**.go'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- .github/workflows/integration.yml
|
||||
- .golangci.yaml
|
||||
- .goreleaser.yaml
|
||||
- container-structure-test.yaml
|
||||
- Dockerfile
|
||||
- .dockerignore
|
||||
- go.mod
|
||||
- go.sum
|
||||
- '**.go'
|
||||
|
||||
env:
|
||||
GOLANG_VERSION: "1.20"
|
||||
GOLANG_FLAGS: -race -mod=readonly
|
||||
WORK_DIR: /usr/src
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
metadata:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: calculate version
|
||||
uses: paulhatch/semantic-version@v5.0.3
|
||||
id: version
|
||||
with:
|
||||
branch: ${{ github.ref_name }}
|
||||
bump_each_commit: false
|
||||
change_path: >-
|
||||
cmd/handler
|
||||
internal
|
||||
go.mod
|
||||
go.sum
|
||||
major_pattern: /^BREAKING CHANGE:|^[^()!:]+(?:\([^()!:]+\))?!:/
|
||||
minor_pattern: /^feat(?:\([^()!:]+\))?:/
|
||||
search_commit_body: true
|
||||
user_format_type: csv
|
||||
version_format: ${major}.${minor}.${patch}-rc.${increment}
|
||||
- name: generate container meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
context: workflow
|
||||
images: ${{ github.repository }}
|
||||
flavor: |
|
||||
latest=true
|
||||
# yamllint disable rule:line-length
|
||||
labels: |
|
||||
org.opencontainers.image.documentation=https://github.com/${{ github.repository }}/blob/master/README.md
|
||||
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||
org.opencontainers.image.url=https://hub.docker.com/r/${{ github.repository }}
|
||||
org.opencontainers.image.version=${{ steps.version.outputs.version }}
|
||||
# yamllint enable rule:line-length
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=raw,value=${{ env.BRANCH }}
|
||||
type=semver,pattern={{version}}
|
||||
github-token: ${{ github.token }}
|
||||
outputs:
|
||||
major: ${{ steps.version.outputs.major }}
|
||||
minor: ${{ steps.version.outputs.minor }}
|
||||
patch: ${{ steps.version.outputs.patch }}
|
||||
increment: ${{ steps.version.outputs.increment }}
|
||||
version_type: ${{ steps.version.outputs.version_type }}
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
tag: ${{ steps.version.outputs.version_tag }}
|
||||
revision: ${{ steps.version.outputs.current_commit }}
|
||||
authors: ${{ steps.version.outputs.authors }}
|
||||
container-labels: ${{ steps.meta.outputs.labels }}
|
||||
container-tags: ${{ steps.meta.outputs.tags }}
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: metadata
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: setup golang
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.GOLANG_VERSION }}
|
||||
- name: set golang environment variables
|
||||
uses: wwmoraes/actions/golang/env@master
|
||||
- name: cache modules
|
||||
uses: pat-s/always-upload-cache@v2.1.5
|
||||
with:
|
||||
path: ${GOMODCACHE}
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-modules-${{ hashFiles('go.sum') }}
|
||||
${{ runner.os }}-modules-
|
||||
- name: download modules
|
||||
run: go mod download
|
||||
- name: cache build
|
||||
uses: pat-s/always-upload-cache@v2.1.5
|
||||
with:
|
||||
path: ${GOCACHE}
|
||||
key: ${{ runner.os }}-build-${{ hashFiles('**/*.go') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ hashFiles('**/*.go') }}
|
||||
${{ runner.os }}-build-
|
||||
- name: generate
|
||||
run: go generate ./...
|
||||
env:
|
||||
VERSION: ${{ needs.metadata.outputs.version }}+${{ github.sha }}
|
||||
- name: build
|
||||
uses: wwmoraes/actions/golang/build@master
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: setup golang
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.GOLANG_VERSION }}
|
||||
- name: set golang environment variables
|
||||
uses: wwmoraes/actions/golang/env@master
|
||||
- name: cache modules
|
||||
uses: pat-s/always-upload-cache@v2.1.5
|
||||
with:
|
||||
path: ${GOMODCACHE}
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-modules-${{ hashFiles('go.sum') }}
|
||||
${{ runner.os }}-modules-
|
||||
- name: download modules
|
||||
run: go mod download
|
||||
- name: cache lint
|
||||
uses: pat-s/always-upload-cache@v2.1.5
|
||||
with:
|
||||
path: ${HOME}/.cache/golangci-lint
|
||||
key: ${{ runner.os }}-lint-${{ hashFiles('.golangci.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-lint-${{ hashFiles('.golangci.yaml') }}
|
||||
${{ runner.os }}-lint-
|
||||
- name: lint
|
||||
uses: wwmoraes/actions/golang/lint@master
|
||||
id: lint
|
||||
with:
|
||||
work-dir: ${{ env.WORK_DIR }}
|
||||
version: v1.46-alpine
|
||||
- name: upload lint report
|
||||
uses: actions/upload-artifact@v2
|
||||
if: always()
|
||||
with:
|
||||
name: lint-report
|
||||
path: ${{ steps.lint.outputs.report-file }}
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: setup golang
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.GOLANG_VERSION }}
|
||||
- name: set golang environment variables
|
||||
uses: wwmoraes/actions/golang/env@master
|
||||
- name: cache modules
|
||||
uses: pat-s/always-upload-cache@v2.1.5
|
||||
with:
|
||||
path: ${GOMODCACHE}
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-modules-${{ hashFiles('go.sum') }}
|
||||
${{ runner.os }}-modules-
|
||||
- name: download modules
|
||||
run: go mod download
|
||||
- name: cache test
|
||||
uses: pat-s/always-upload-cache@v2.1.5
|
||||
with:
|
||||
path: ${GOCACHE}
|
||||
key: ${{ runner.os }}-test-${{ hashFiles('**/*.go') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-test-${{ hashFiles('**/*.go') }}
|
||||
${{ runner.os }}-test-
|
||||
- name: test
|
||||
uses: wwmoraes/actions/golang/test@master
|
||||
id: test
|
||||
- name: security scan
|
||||
uses: anchore/scan-action@v3
|
||||
with:
|
||||
path: .
|
||||
fail-build: true
|
||||
- name: upload coverage report
|
||||
uses: actions/upload-artifact@v2
|
||||
if: always()
|
||||
with:
|
||||
name: coverage-report
|
||||
path: ${{ steps.test.outputs.cover-profile }}
|
||||
- name: upload test report
|
||||
uses: actions/upload-artifact@v2
|
||||
if: always()
|
||||
with:
|
||||
name: test-report
|
||||
path: ${{ steps.test.outputs.report-file }}
|
||||
report:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, test]
|
||||
if: always()
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: cache sonar scanner
|
||||
uses: pat-s/always-upload-cache@v2.1.5
|
||||
with:
|
||||
path: ${{ runner.temp }}/sonar-scanner/cache
|
||||
# yamllint disable-line rule:line-length
|
||||
key: ${{ runner.os }}-sonar-scanner-cache-${{ hashFiles('**/sonar-project.properties') }}
|
||||
# yamllint disable rule:line-length
|
||||
restore-keys: |
|
||||
${{ runner.os }}-sonar-scanner-cache-${{ hashFiles('**/sonar-project.properties') }}
|
||||
${{ runner.os }}-sonar-scanner-cache-
|
||||
# yamllint enable rule:line-length
|
||||
- name: download lint report
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: lint-report
|
||||
- name: download test report
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: test-report
|
||||
- name: download coverage report
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: coverage-report
|
||||
- name: run sonar scanner
|
||||
uses: wwmoraes/actions/sonar-scanner@master
|
||||
with:
|
||||
token: ${{ secrets.SONAR_TOKEN }}
|
||||
work-dir: ${{ env.WORK_DIR }}
|
||||
home: ${{ runner.temp }}/sonar-scanner
|
||||
release-binary:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [metadata, build, lint, test]
|
||||
permissions:
|
||||
contents: write
|
||||
if: >-
|
||||
github.event_name == 'push'
|
||||
&& startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: setup golang
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.GOLANG_VERSION }}
|
||||
- name: set golang environment variables
|
||||
uses: wwmoraes/actions/golang/env@master
|
||||
- name: cache modules
|
||||
uses: pat-s/always-upload-cache@v2.1.5
|
||||
with:
|
||||
path: ${GOMODCACHE}
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-modules-${{ hashFiles('go.sum') }}
|
||||
${{ runner.os }}-modules-
|
||||
- name: cache build
|
||||
uses: pat-s/always-upload-cache@v2.1.5
|
||||
with:
|
||||
path: ${GOCACHE}
|
||||
key: ${{ runner.os }}-build-${{ hashFiles('**/*.go') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ hashFiles('**/*.go') }}
|
||||
${{ runner.os }}-build-
|
||||
- name: generate
|
||||
run: go generate ./...
|
||||
env:
|
||||
VERSION: ${{ needs.metadata.outputs.version }}
|
||||
- name: run goreleaser
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
args: release --clean --skip-validate
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: metadata
|
||||
env:
|
||||
# runner context is not available here...
|
||||
GRYPE_DB_CACHE_TEMP_PATH: .cache/grype/db/
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: set up docker buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: cache buildx
|
||||
uses: pat-s/always-upload-cache@v2.1.5
|
||||
with:
|
||||
path: ${{ runner.temp }}/.buildx-cache
|
||||
# yamllint disable-line rule:line-length
|
||||
key: ${{ runner.os }}-buildx-${{ hashFiles('Dockerfile', '.dockerignore') }}
|
||||
# yamllint disable rule:line-length
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-${{ hashFiles('Dockerfile', '.dockerignore') }}
|
||||
${{ runner.os }}-buildx-
|
||||
# yamllint enable rule:line-length
|
||||
- name: build single-arch test image
|
||||
uses: docker/build-push-action@v3
|
||||
env:
|
||||
DOCKER_BUILDKIT: 0
|
||||
BUILDKIT_INLINE_CACHE: 1
|
||||
with:
|
||||
push: false
|
||||
load: true
|
||||
labels: ${{ needs.metadata.outputs.container-labels }}
|
||||
cache-to: |
|
||||
type=local,mode=max,dest=${{ runner.temp }}/.buildx-cache-new
|
||||
cache-from: |
|
||||
type=local,src=${{ runner.temp }}/.buildx-cache
|
||||
${{ needs.metadata.outputs.container-tags }}
|
||||
${{ github.repository }}:test
|
||||
tags: ${{ github.repository }}:test
|
||||
build-args: |
|
||||
GOLANG_VERSION=${{ env.GOLANG_VERSION }}
|
||||
VERSION=${{ needs.metadata.outputs.version }}
|
||||
# fix to prevent ever-growing caches
|
||||
# https://github.com/docker/build-push-action/issues/252
|
||||
# https://github.com/moby/buildkit/issues/1896
|
||||
- name: Move cache
|
||||
run: |
|
||||
rm -rf ${{ runner.temp }}/.buildx-cache
|
||||
mv ${{ runner.temp }}/.buildx-cache-new ${{ runner.temp }}/.buildx-cache
|
||||
- name: test structure
|
||||
uses: brpaz/structure-tests-action@v1.1.2
|
||||
with:
|
||||
image: ${{ github.repository }}:test
|
||||
configFile: container-structure-test.yaml
|
||||
- name: cache grype
|
||||
uses: pat-s/always-upload-cache@v2.1.5
|
||||
with:
|
||||
path: ${{ runner.temp }}/${{ env.GRYPE_DB_CACHE_TEMP_PATH }}
|
||||
key: ${{ runner.os }}-grype-${{ hashFiles('.grype.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-grype-${{ hashFiles('.grype.yaml') }}
|
||||
${{ runner.os }}-grype-
|
||||
- name: grype scan
|
||||
uses: anchore/scan-action@v3
|
||||
with:
|
||||
image: ${{ github.repository }}:test
|
||||
fail-build: true
|
||||
publish-image:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [metadata, docker, lint, test]
|
||||
if: >-
|
||||
github.event_name == 'push'
|
||||
&& startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: set up docker buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: cache buildx
|
||||
uses: pat-s/always-upload-cache@v2.1.5
|
||||
with:
|
||||
path: ${{ runner.temp }}/.buildx-cache
|
||||
# yamllint disable-line rule:line-length
|
||||
key: ${{ runner.os }}-buildx-${{ hashFiles('Dockerfile', '.dockerignore') }}
|
||||
# yamllint disable rule:line-length
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-${{ hashFiles('Dockerfile', '.dockerignore') }}
|
||||
${{ runner.os }}-buildx-
|
||||
# yamllint enable rule:line-length
|
||||
# fix to prevent ever-growing caches
|
||||
# https://github.com/docker/build-push-action/issues/252
|
||||
# https://github.com/moby/buildkit/issues/1896
|
||||
- name: login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: push multi-arch image
|
||||
uses: docker/build-push-action@v3
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
BUILDKIT_INLINE_CACHE: 1
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||
labels: ${{ needs.metadata.outputs.container-labels }}
|
||||
cache-to: |
|
||||
type=local,mode=max,dest=${{ runner.temp }}/.buildx-cache-new
|
||||
cache-from: |
|
||||
type=local,src=${{ runner.temp }}/.buildx-cache
|
||||
${{ needs.metadata.outputs.container-tags }}
|
||||
${{ github.repository }}:master
|
||||
${{ github.repository }}:latest
|
||||
build-args: |
|
||||
GOLANG_VERSION=${{ env.GOLANG_VERSION }}
|
||||
VERSION=${{ needs.metadata.outputs.version }}
|
||||
tags: ${{ needs.metadata.outputs.container-tags }}
|
||||
# fix to prevent ever-growing caches
|
||||
# https://github.com/docker/build-push-action/issues/252
|
||||
# https://github.com/moby/buildkit/issues/1896
|
||||
- name: Move cache
|
||||
run: |
|
||||
rm -rf ${{ runner.temp }}/.buildx-cache
|
||||
mv ${{ runner.temp }}/.buildx-cache-new ${{ runner.temp }}/.buildx-cache
|
||||
- name: update DockerHub description
|
||||
uses: meeDamian/sync-readme@v1.0.6
|
||||
with:
|
||||
pass: ${{ secrets.DOCKER_PASSWORD }}
|
||||
description: true
|
54
.gitignore
vendored
Normal file
54
.gitignore
vendored
Normal file
|
@ -0,0 +1,54 @@
|
|||
*.env
|
||||
*.env.local
|
||||
bin/
|
||||
dist/
|
||||
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
19
.golangci.yaml
Normal file
19
.golangci.yaml
Normal file
|
@ -0,0 +1,19 @@
|
|||
run:
|
||||
modules-download-mode: readonly
|
||||
issues-exit-code: 0
|
||||
timeout: 2m
|
||||
output:
|
||||
format: colored-line-number
|
||||
linters-settings:
|
||||
enable:
|
||||
- stylecheck
|
||||
- unconvert
|
||||
- gosec
|
||||
- errorlint
|
||||
- maligned
|
||||
- interfacer
|
||||
- unparam
|
||||
- whitespace
|
||||
- wrapcheck
|
||||
- prealloc
|
||||
- nolintlint
|
38
.goreleaser.yml
Normal file
38
.goreleaser.yml
Normal file
|
@ -0,0 +1,38 @@
|
|||
before:
|
||||
hooks:
|
||||
- go mod download
|
||||
builds:
|
||||
- id: handler
|
||||
main: ./cmd/handler
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- 7
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-rc"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- '^ci:'
|
||||
release:
|
||||
github:
|
||||
owner: wwmoraes
|
||||
name: anilistarr
|
||||
prerelease: auto
|
68
.pre-commit-config.yaml
Normal file
68
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,68 @@
|
|||
## See https://pre-commit.com for more information
|
||||
## See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-case-conflict
|
||||
- id: check-merge-conflict
|
||||
args:
|
||||
- --assume-in-merge
|
||||
- id: check-vcs-permalinks
|
||||
- id: check-yaml
|
||||
args: [--allow-multiple-documents]
|
||||
- id: detect-private-key
|
||||
# - id: end-of-file-fixer
|
||||
- id: fix-byte-order-marker
|
||||
- id: mixed-line-ending
|
||||
# - id: no-commit-to-branch
|
||||
- id: trailing-whitespace
|
||||
- repo: https://gitlab.com/bmares/check-json5
|
||||
rev: v1.0.0
|
||||
hooks:
|
||||
- id: check-json5
|
||||
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
|
||||
rev: 2.4.0
|
||||
hooks:
|
||||
- id: editorconfig-checker
|
||||
exclude: vscode/.config/Code/User/globalStorage/.*
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
rev: v0.32.1
|
||||
hooks:
|
||||
- id: markdownlint
|
||||
- repo: https://github.com/hadolint/hadolint
|
||||
rev: v2.10.0
|
||||
hooks:
|
||||
- id: hadolint
|
||||
- repo: https://github.com/tekwizely/pre-commit-golang
|
||||
rev: v1.0.0-beta.5
|
||||
hooks:
|
||||
- id: golangci-lint-repo-mod
|
||||
name: golangci-lint
|
||||
- id: go-mod-tidy
|
||||
name: go mod tidy
|
||||
- id: go-build-repo-mod
|
||||
name: go build
|
||||
- id: go-test-repo-mod
|
||||
name: go test
|
||||
##
|
||||
## Invoking Custom Go Tools
|
||||
## - Configured *entirely* through the `args` attribute, ie:
|
||||
## args: [ go, test, ./... ]
|
||||
## - Use the `name` attribute to provide better messaging when the hook runs
|
||||
## - Use the `alias` attribute to be able invoke your hook via `pre-commit run`
|
||||
##
|
||||
# - id: my-cmd
|
||||
# - id: my-cmd-mod
|
||||
# - id: my-cmd-pkg
|
||||
# - id: my-cmd-repo
|
||||
# - id: my-cmd-repo-mod
|
||||
# - id: my-cmd-repo-pkg
|
||||
ci:
|
||||
skip:
|
||||
- hadolint
|
||||
- golangci-lint-repo-mod
|
||||
- go-mod-tidy
|
||||
- go-build-repo-mod
|
||||
- go-test-repo-mod
|
55
.vscode/settings.json
vendored
Normal file
55
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"cSpell.words": [
|
||||
"Anidb",
|
||||
"anilist",
|
||||
"anilistarr",
|
||||
"Anisearch",
|
||||
"bbolt",
|
||||
"fribbs",
|
||||
"genqlient",
|
||||
"HTTPURL",
|
||||
"Imdb",
|
||||
"jmoiron",
|
||||
"Kitsu",
|
||||
"Livechart",
|
||||
"logr",
|
||||
"modernc",
|
||||
"nolint",
|
||||
"nosniff",
|
||||
"opentelemetry",
|
||||
"otel",
|
||||
"otelhttp",
|
||||
"otlp",
|
||||
"otlpmetric",
|
||||
"otlpmetricgrpc",
|
||||
"otlpr",
|
||||
"otlptrace",
|
||||
"otlptracegrpc",
|
||||
"OWASP",
|
||||
"promhttp",
|
||||
"redisotel",
|
||||
"romaji",
|
||||
"sdkmetric",
|
||||
"sdktrace",
|
||||
"semconv",
|
||||
"Sonarr",
|
||||
"sqlx",
|
||||
"themoviedb",
|
||||
"thetvdb",
|
||||
"Tmdb",
|
||||
"tvdb",
|
||||
"Upsert",
|
||||
"usecases"
|
||||
],
|
||||
"go.coverageDecorator": {
|
||||
"type": "highlight",
|
||||
"coveredHighlightColor": "rgba(64,128,128,0.5)",
|
||||
"uncoveredHighlightColor": "rgba(128,64,64,0.25)",
|
||||
"coveredBorderColor": "rgba(64,128,128,0.5)",
|
||||
"uncoveredBorderColor": "rgba(128,64,64,0.25)",
|
||||
"coveredGutterStyle": "blockblue",
|
||||
"uncoveredGutterStyle": "slashyellow"
|
||||
},
|
||||
"go.coverageOptions": "showBothCoveredAndUncoveredCode",
|
||||
"go.coverMode": "atomic"
|
||||
}
|
77
CODE_OF_CONDUCT.md
Normal file
77
CODE_OF_CONDUCT.md
Normal file
|
@ -0,0 +1,77 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at github@artero.dev. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 1.4, available at
|
||||
<https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
<https://www.contributor-covenant.org/faq>
|
146
CONTRIBUTING.md
Normal file
146
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,146 @@
|
|||
# Contributing
|
||||
|
||||
When contributing to this repository, please first discuss the change you wish
|
||||
to make via issue, email, or any other method with the owners of this repository
|
||||
before making a change.
|
||||
|
||||
Please note we have a code of conduct, please follow it in all your interactions
|
||||
with the project.
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. Ensure any install or build dependencies are removed before the end of the
|
||||
layer when doing a build.
|
||||
2. Update the README.md with details of changes to the interface, this includes
|
||||
new environment variables, exposed ports, useful file locations and container
|
||||
parameters.
|
||||
3. Increase the version numbers in any examples files and the README.md to the
|
||||
new version that this Pull Request would represent. The versioning scheme we use
|
||||
is [SemVer](http://semver.org/).
|
||||
4. You may merge the Pull Request in once you have the sign-off of two other
|
||||
developers, or if you do not have permission to do that, you may request the
|
||||
second reviewer to merge it for you.
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity and
|
||||
orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement. All complaints
|
||||
will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
project community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at <https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.
|
||||
|
||||
Community Impact Guidelines were inspired by Mozilla's
|
||||
[code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
<https://www.contributor-covenant.org/faq>. Translations are available at
|
||||
<https://www.contributor-covenant.org/translations>.
|
32
Dockerfile
Normal file
32
Dockerfile
Normal file
|
@ -0,0 +1,32 @@
|
|||
ARG GOLANG_VERSION=1.20
|
||||
FROM golang:${GOLANG_VERSION}-alpine AS build
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download -x
|
||||
|
||||
COPY . .
|
||||
ARG VERSION
|
||||
RUN go generate ./... && go build -o ./bin/handler ./cmd/handler/...
|
||||
|
||||
|
||||
FROM scratch
|
||||
|
||||
LABEL org.opencontainers.image.authors="William Artero <docker@artero.dev>"
|
||||
LABEL org.opencontainers.image.description="anilist custom list provider for sonarr/radarr"
|
||||
LABEL org.opencontainers.image.licenses="MIT"
|
||||
LABEL org.opencontainers.image.title="Anilistarr"
|
||||
LABEL org.opencontainers.image.vendor="William Artero <docker@artero.dev>"
|
||||
|
||||
CMD ["/usr/local/bin/handler"]
|
||||
EXPOSE 8080
|
||||
|
||||
ARG GOLANG_VERSION
|
||||
LABEL org.opencontainers.image.base.name="golang:${GOLANG_VERSION}-alpine"
|
||||
ARG VERSION
|
||||
LABEL org.opencontainers.image.version="${VERSION}"
|
||||
|
||||
COPY --from=build /src/bin/handler /usr/local/bin/handler
|
||||
|
||||
USER 20000:20000
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 William Artero
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
62
Makefile
Normal file
62
Makefile
Normal file
|
@ -0,0 +1,62 @@
|
|||
-include .env
|
||||
-include .env.local
|
||||
export
|
||||
|
||||
CMD_SOURCE_FILES := $(shell find cmd -type f -name '*.go')
|
||||
INTERNAL_SOURCE_FILES := $(shell find internal -type f -name '*.go')
|
||||
SOURCE_FILES := $(CMD_SOURCE_FILES) $(INTERNAL_SOURCE_FILES)
|
||||
|
||||
.PHONY: build
|
||||
build: generate
|
||||
go build -o ./bin/ ./...
|
||||
|
||||
.PHONY: release
|
||||
release:
|
||||
goreleaser release --clean --skip-publish --skip-announce --snapshot
|
||||
|
||||
.PHONY: generate
|
||||
generate:
|
||||
go generate ./...
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -race -v ./...
|
||||
|
||||
.PHONY: coverage
|
||||
coverage: coverage.out
|
||||
@go tool cover -func=$<
|
||||
|
||||
%.out: $(SOURCE_FILES)
|
||||
@go test -race -cover -coverprofile=$@ -v ./...
|
||||
|
||||
IMAGE ?= wwmoraes/anilistarr
|
||||
# needs go install github.com/Khan/genqlient@latest
|
||||
anilist:
|
||||
@cd internal/drivers/anilist && genqlient
|
||||
|
||||
image: CREATED=$(shell date -u +"%Y-%m-%dT%TZ")
|
||||
image: REVISION=$(shell git log -n 1 --format="%H")
|
||||
image: VERSION=$(patsubst v%,%,$(shell git describe --tags 2> /dev/null || echo "0.1.0-rc.0"))
|
||||
image:
|
||||
docker build --load $(if ${TARGET},--target ${TARGET}) \
|
||||
-t ${IMAGE} \
|
||||
--build-arg VERSION=${VERSION} \
|
||||
--label org.opencontainers.image.created=${CREATED} \
|
||||
--label org.opencontainers.image.revision=${REVISION} \
|
||||
--label org.opencontainers.image.documentation=https://github.com/${IMAGE}/blob/master/README.md \
|
||||
--label org.opencontainers.image.source=https://github.com/${IMAGE} \
|
||||
--label org.opencontainers.image.url=https://hub.docker.com/r/${IMAGE} \
|
||||
.
|
||||
@container-structure-test test -c container-structure-test.yaml -i wwmoraes/anilistarr
|
||||
|
||||
run:
|
||||
@go run ./cmd/handler/...
|
||||
|
||||
redis-cli:
|
||||
@redis-cli -p 16379
|
||||
|
||||
redis-proxy:
|
||||
@flyctl redis proxy
|
||||
|
||||
get-user:
|
||||
@curl -v "http://127.0.0.1:8080/user?name=wwmoraes"
|
83
README.md
Normal file
83
README.md
Normal file
|
@ -0,0 +1,83 @@
|
|||
# anilistarr
|
||||
|
||||
> anilist custom list provider for sonarr/radarr
|
||||
|
||||
![Status](https://img.shields.io/badge/status-active-success.svg)
|
||||
[![GitHub Issues](https://img.shields.io/github/issues/wwmoraes/anilistarr.svg)](https://github.com/wwmoraes/anilistarr/issues)
|
||||
[![GitHub Pull Requests](https://img.shields.io/github/issues-pr/wwmoraes/anilistarr.svg)](https://github.com/wwmoraes/anilistarr/pulls)
|
||||
|
||||
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/wwmoraes/anilistarr/master.svg)](https://results.pre-commit.ci/latest/github/wwmoraes/anilistarr/master)
|
||||
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=wwmoraes_anilistarr&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=wwmoraes_anilistarr)
|
||||
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=wwmoraes_anilistarr&metric=coverage)](https://sonarcloud.io/summary/new_code?id=wwmoraes_anilistarr)
|
||||
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=wwmoraes_anilistarr&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=wwmoraes_anilistarr)
|
||||
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=wwmoraes_anilistarr&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=wwmoraes_anilistarr)
|
||||
|
||||
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](/LICENSE)
|
||||
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwwmoraes%2Fanilistarr.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwwmoraes%2Fanilistarr?ref=badge_shield)
|
||||
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/7718/badge)](https://bestpractices.coreinfrastructure.org/projects/7718)
|
||||
|
||||
[![Docker Image Size (latest semver)](https://img.shields.io/docker/image-size/wwmoraes/anilistarr)](https://hub.docker.com/r/wwmoraes/anilistarr)
|
||||
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/wwmoraes/anilistarr?label=image%20version)](https://hub.docker.com/r/wwmoraes/anilistarr)
|
||||
[![Docker Pulls](https://img.shields.io/docker/pulls/wwmoraes/anilistarr)](https://hub.docker.com/r/wwmoraes/anilistarr)
|
||||
|
||||
---
|
||||
|
||||
## 📝 Table of Contents
|
||||
|
||||
- [About](#-about)
|
||||
- [Getting Started](#-getting-started)
|
||||
- [Deployment](#-deployment)
|
||||
- [Usage](#-usage)
|
||||
- [Built Using](#-built-using)
|
||||
- [TODO](./TODO.md)
|
||||
- [Contributing](./CONTRIBUTING.md)
|
||||
- [Authors](#-authors)
|
||||
- [Acknowledgments](#-acknowledgements)
|
||||
|
||||
## 🧐 About
|
||||
|
||||
Converts an Anilist user watching list to a custom list format which *arr apps support.
|
||||
|
||||
It works by fetching the user info directly from Anilist thanks to its API, and
|
||||
converts the IDs using community-provided mappings.
|
||||
|
||||
## 🏁 Getting Started
|
||||
|
||||
Clone the repository and use `go run ./cmd/handler/...` to get the REST API up.
|
||||
|
||||
## 🔧 Running the tests
|
||||
|
||||
Explain how to run the automated tests for this system.
|
||||
|
||||
## 🎈 Usage
|
||||
|
||||
Configuration in general is a WIP. The code supports distinct storage and cache
|
||||
options and even has built-in support for Redis and Bolt as caches already.
|
||||
The handler needs flags/configuration file support to allow switching at
|
||||
runtime.
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
The `handler` binary is statically compiled and serves both the REST API and the
|
||||
telemetry to an OTLP endpoint. Extra requirements depend on which storage and
|
||||
cache technologies you've chosen; e.g. using SQLite/Bolt requires a database
|
||||
file. The Docker image provided contains the handler alone, for instance.
|
||||
|
||||
## 🔧 Built Using
|
||||
|
||||
- [Golang](https://go.dev) - Base language
|
||||
- [Chi](https://go-chi.io) - net/HTTP-compatible router that doesn't suck
|
||||
- [genqlient](https://github.com/Khan/genqlient) - type-safe GraphQL client generator
|
||||
- [xo](https://github.com/xo/xo) - SQL client code generator
|
||||
- [Open Telemetry](https://opentelemetry.io) - Observability
|
||||
|
||||
## 🧑💻 Authors
|
||||
|
||||
- [@wwmoraes](https://github.com/wwmoraes) - Idea & Initial work
|
||||
|
||||
## 🎉 Acknowledgements
|
||||
|
||||
- Anilist for their great service and API <https://anilist.gitbook.io/anilist-apiv2-docs/>
|
||||
- The community for their efforts to map IDs between services
|
||||
- <https://github.com/Fribb/anime-lists>
|
||||
- <https://github.com/Anime-Lists/anime-lists/>
|
47
SECURITY.md
Normal file
47
SECURITY.md
Normal file
|
@ -0,0 +1,47 @@
|
|||
# Security Guidelines
|
||||
|
||||
## How security is managed on this project
|
||||
|
||||
Contributors take security seriously and wants to ensure that we maintain a
|
||||
secure environment for our customers and that we also provide secure solutions
|
||||
for the open source community. To help us achieve these goals, please note the
|
||||
following before using this software:
|
||||
|
||||
- Review the software license to understand the contributor's obligations in
|
||||
terms of warranties and suitability for purpose
|
||||
- For any questions or concerns about security, you can
|
||||
[create an issue][new-issue] or [report a vulnerability][new-sec-issue]
|
||||
- We request that you work with our security team and opt for
|
||||
[responsible disclosure][disclosure] using the guidelines below
|
||||
- All security related issues and pull requests you make should be tagged with
|
||||
"security" for easy identification
|
||||
- Please monitor this repository and update your environment in a timely manner
|
||||
as we release patches and updates
|
||||
|
||||
## Responsibly Disclosing Security Bugs
|
||||
|
||||
If you find a security bug in this repository, please work with contributors
|
||||
following responsible disclosure principles and these guidelines:
|
||||
|
||||
- Do not submit a normal issue or pull request in our public repository, instead
|
||||
[report it directly][new-sec-issue].
|
||||
- We will review your submission and may follow up for additional details
|
||||
- If you have a patch, we will review it and approve it privately; once approved
|
||||
for release you can submit it as a pull request publicly in the repository (we
|
||||
give credit where credit is due)
|
||||
- We will keep you informed during our investigation, feel free to check in for
|
||||
a status update
|
||||
- We will release the fix and publicly disclose the issue as soon as possible,
|
||||
but want to ensure we due properly due diligence before releasing
|
||||
- Please do not publicly blog or post about the security issue until after we
|
||||
have updated the public repo so that other downstream users have an opportunity
|
||||
to patch
|
||||
|
||||
## Contact / Misc
|
||||
|
||||
If you have any questions, please reach out directly by
|
||||
[creating an issue][new-issue].
|
||||
|
||||
[new-issue]: https://github.com/wwmoraes/anilistarr/issues/new/choose
|
||||
[new-sec-issue]: https://github.com/wwmoraes/anilistarr/security/advisories/new
|
||||
[disclosure]: https://corporate.walmart.com/article/responsible-disclosure-policy
|
28
TODO.md
Normal file
28
TODO.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
# to-dos
|
||||
|
||||
## security
|
||||
|
||||
- [ ] OWASP ZAP scan <https://github.com/marketplace?type=actions&query=publisher%3Azaproxy+>
|
||||
|
||||
## code
|
||||
|
||||
### Telemetry
|
||||
|
||||
- <https://github.com/riandyrn/otelchi>
|
||||
- <https://github.com/MrAlias/flow>
|
||||
|
||||
### store
|
||||
|
||||
- <https://github.com/objectbox/objectbox-go>
|
||||
|
||||
### providers
|
||||
|
||||
- <https://github.com/Anime-Lists/anime-lists>
|
||||
|
||||
## environment
|
||||
|
||||
### production
|
||||
|
||||
- <https://fly.io/docs/postgres/>
|
||||
- <https://fly.io/docs/app-guides/custom-domains-with-fly/>
|
||||
- <https://github.com/superfly/litefs-example/blob/main/fly-io-config/etc/litefs.yml>
|
142
anilistarr.puml
Normal file
142
anilistarr.puml
Normal file
|
@ -0,0 +1,142 @@
|
|||
@startuml components
|
||||
|
||||
package entities {
|
||||
struct Media {
|
||||
AnilistID uint64
|
||||
TvdbID uint64
|
||||
Type string
|
||||
}
|
||||
|
||||
struct SonarrCustomEntry {
|
||||
TvdbID uint64
|
||||
}
|
||||
|
||||
struct SonarrCustomList <<alias>> {
|
||||
[]SonarrCustomEntry
|
||||
}
|
||||
}
|
||||
|
||||
package usecases {
|
||||
interface Mapper {
|
||||
+MapIDs([]string) []string
|
||||
+MapID(string) string
|
||||
+Refresh()
|
||||
}
|
||||
|
||||
interface Tracker {
|
||||
+GetUserID(name string) string
|
||||
+GetMediaList(userId string) []Media
|
||||
}
|
||||
|
||||
class MediaLinker {
|
||||
+GenerateCustomList(name string) SonarrCustomList
|
||||
+GetUserID(name string) string
|
||||
}
|
||||
}
|
||||
|
||||
package adapters {
|
||||
package mapper <<Frame>> {
|
||||
interface MetadataSource {
|
||||
Fetch()
|
||||
}
|
||||
|
||||
interface Metadata {
|
||||
GetAnilistID() string
|
||||
GetTvdbID() string
|
||||
}
|
||||
|
||||
interface Store {
|
||||
PutMedia(Context, Media)
|
||||
}
|
||||
|
||||
interface AnilistStore {
|
||||
MappingByAnilistID(Context, string) Media
|
||||
}
|
||||
|
||||
class JSONFile
|
||||
class JSONURL
|
||||
class AnilistMapper
|
||||
}
|
||||
|
||||
package cache <<Frame>> {
|
||||
interface Cache {
|
||||
GetString(ctx, key) string
|
||||
SetString(ctx, key, value)
|
||||
}
|
||||
|
||||
class CachedTracker
|
||||
}
|
||||
}
|
||||
|
||||
package drivers {
|
||||
package providers <<Frame>> {
|
||||
struct FribbsEntry
|
||||
entity FribbsSource
|
||||
entity LocalJSON
|
||||
}
|
||||
|
||||
package stores <<Frame>> {
|
||||
class Sql
|
||||
}
|
||||
|
||||
package caches <<Frame>> {
|
||||
class Redis
|
||||
class Bolt
|
||||
}
|
||||
|
||||
package trackers <<Frame>> {
|
||||
class Anilist
|
||||
}
|
||||
}
|
||||
|
||||
package cmd {
|
||||
package api {
|
||||
class RestAPI <<net.http>>
|
||||
entity "main" as apiMain
|
||||
|
||||
RestAPI o-- apiMain
|
||||
}
|
||||
|
||||
package cli {
|
||||
entity "main" as cliMain
|
||||
}
|
||||
}
|
||||
|
||||
'' visual hack to force both outer-level packages on the same rank
|
||||
drivers -[hidden] cmd
|
||||
|
||||
'' entities
|
||||
SonarrCustomEntry --* SonarrCustomList
|
||||
'' use-cases
|
||||
Media <-- MediaLinker
|
||||
SonarrCustomList <-- MediaLinker
|
||||
MediaLinker o--> Mapper
|
||||
MediaLinker o--> Tracker
|
||||
'' adapters/mapper
|
||||
Mapper <|-[dashed]- AnilistMapper
|
||||
AnilistMapper o--> Metadata
|
||||
AnilistMapper o--> MetadataSource
|
||||
AnilistMapper o--> AnilistStore
|
||||
Store <|-[dashed]- AnilistStore
|
||||
MetadataSource <|-[dashed]- JSONFile
|
||||
MetadataSource <|-[dashed]- JSONURL
|
||||
'' adapters/cache
|
||||
Tracker <|-[dashed]- CachedTracker
|
||||
CachedTracker o--> Cache
|
||||
CachedTracker o--> Tracker
|
||||
'' drivers/providers
|
||||
FribbsEntry -* FribbsSource
|
||||
JSONURL <|-- FribbsSource
|
||||
JSONFile <|-- LocalJSON
|
||||
Metadata <|-[dashed]- FribbsEntry
|
||||
'' drivers/stores
|
||||
AnilistStore <|-[dashed]- Sql
|
||||
'' drivers/caches
|
||||
Cache <|-[dashed]- Bolt
|
||||
Cache <|-[dashed]- Redis
|
||||
'' drivers/trackers
|
||||
Tracker <|-[dashed]- Anilist
|
||||
'' cmd
|
||||
MediaLinker <--o RestAPI
|
||||
|
||||
@enduml
|
37
cmd/handler/limiter.go
Normal file
37
cmd/handler/limiter.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/wwmoraes/anilistarr/internal/telemetry"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
func Limiter(limiter *rate.Limiter) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
span := telemetry.SpanFromContext(r.Context())
|
||||
|
||||
re := limiter.Reserve()
|
||||
if !re.OK() {
|
||||
err := fmt.Errorf("misconfigured rate limiter on the API, cannot act")
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if re.Delay() > 0 {
|
||||
re.Cancel()
|
||||
w.Header().Add("Retry-After", strconv.FormatFloat(math.Ceil(re.Delay().Seconds()), 'f', 0, 64))
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
116
cmd/handler/main.go
Normal file
116
cmd/handler/main.go
Normal file
|
@ -0,0 +1,116 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/wwmoraes/anilistarr/internal/drivers/caches"
|
||||
"github.com/wwmoraes/anilistarr/internal/telemetry"
|
||||
"github.com/wwmoraes/anilistarr/internal/usecases"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type ServerContext string
|
||||
|
||||
const ListenerAddressKey ServerContext = "listener-address"
|
||||
|
||||
func main() {
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
log := telemetry.DefaultLogger()
|
||||
log.Info("staring up", "name", telemetry.NAME, "version", telemetry.VERSION)
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
addr := "127.0.0.1:" + port
|
||||
|
||||
mapper, err := NewAnilistBridge(
|
||||
os.Getenv("ANILIST_GRAPHQL_ENDPOINT"),
|
||||
&caches.RedisOptions{
|
||||
Addr: os.Getenv("REDIS_ADDRESS"),
|
||||
Username: os.Getenv("REDIS_USERNAME"),
|
||||
Password: os.Getenv("REDIS_PASSWORD"),
|
||||
ClientName: "anilistarr-handler",
|
||||
},
|
||||
)
|
||||
assert(err)
|
||||
|
||||
api, err := NewRestAPI(mapper)
|
||||
assert(err)
|
||||
|
||||
shutdown, err := telemetry.InstrumentAll(ctx, os.Getenv("OTLP_ENDPOINT"))
|
||||
assert(err)
|
||||
defer shutdown(context.Background())
|
||||
|
||||
ctx = telemetry.ContextWithLogger(ctx)
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(telemetry.NewHandlerMiddleware)
|
||||
r.Use(Limiter(rate.NewLimiter(rate.Every(time.Minute), 1000)))
|
||||
r.Get("/list", api.GetList)
|
||||
r.Get("/map", api.GetMap)
|
||||
r.Get("/user", api.GetUser)
|
||||
|
||||
server := http.Server{
|
||||
Addr: addr,
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
// update mapping every week
|
||||
go scheduledRefresh(ctx, mapper, time.Hour*24*7)
|
||||
go server.ListenAndServe() //nolint:errcheck
|
||||
log.Info("server listening", "address", addr)
|
||||
|
||||
<-ctx.Done()
|
||||
cancel()
|
||||
|
||||
gracefulShutdown(&server)
|
||||
}
|
||||
|
||||
func assert(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
log := telemetry.DefaultLogger()
|
||||
|
||||
log.Error(err, "assertion failed")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func scheduledRefresh(ctx context.Context, linker *usecases.MediaBridge, interval time.Duration) {
|
||||
log := telemetry.LoggerFromContext(ctx)
|
||||
|
||||
for {
|
||||
log.Info("refreshing linker metadata")
|
||||
err := linker.Refresh(ctx)
|
||||
assert(err)
|
||||
log.Info("linker metadata refreshed")
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Info("scheduled refresh stopped")
|
||||
return
|
||||
case <-time.After(interval):
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func gracefulShutdown(server *http.Server) {
|
||||
log := telemetry.DefaultLogger()
|
||||
|
||||
log.Info("shutting down, press Ctrl+C again to force")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
assert(server.Shutdown(ctx))
|
||||
}
|
141
cmd/handler/restapi.go
Normal file
141
cmd/handler/restapi.go
Normal file
|
@ -0,0 +1,141 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/wwmoraes/anilistarr/internal/entities"
|
||||
"github.com/wwmoraes/anilistarr/internal/telemetry"
|
||||
"github.com/wwmoraes/anilistarr/internal/usecases"
|
||||
)
|
||||
|
||||
var (
|
||||
usernameRegex = regexp.MustCompile("^[[:word:]]+$")
|
||||
)
|
||||
|
||||
type RestAPI interface {
|
||||
GetList(http.ResponseWriter, *http.Request)
|
||||
GetMap(http.ResponseWriter, *http.Request)
|
||||
GetUser(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
type restAPI struct {
|
||||
mapper *usecases.MediaBridge
|
||||
}
|
||||
|
||||
func NewRestAPI(mapper *usecases.MediaBridge) (RestAPI, error) {
|
||||
return &restAPI{
|
||||
mapper: mapper,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (face *restAPI) GetList(w http.ResponseWriter, r *http.Request) {
|
||||
span := telemetry.SpanFromContext(r.Context())
|
||||
|
||||
username := r.URL.Query().Get("username")
|
||||
if !usernameRegex.MatchString(username) {
|
||||
err := fmt.Errorf("invalid username")
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
|
||||
customList, err := face.mapper.GenerateCustomList(r.Context(), username)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := json.Marshal(customList)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err = w.Write(data)
|
||||
span.RecordError(err)
|
||||
}
|
||||
|
||||
func (face *restAPI) GetMap(w http.ResponseWriter, r *http.Request) {
|
||||
span := telemetry.SpanFromContext(r.Context())
|
||||
|
||||
resp, err := http.Get("https://github.com/Fribb/anime-lists/raw/master/anime-list-full.json")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
|
||||
var entries []entities.Media
|
||||
err = json.Unmarshal(data, &entries)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
|
||||
telemetry.Int(span, "entries", len(entries))
|
||||
|
||||
records := make(map[string]string, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.AnilistID == "" || entry.TvdbID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
records[entry.AnilistID] = entry.TvdbID
|
||||
}
|
||||
|
||||
newData, err := json.MarshalIndent(records, "", " ")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintln(w, string(newData))
|
||||
}
|
||||
|
||||
func (face *restAPI) GetUser(w http.ResponseWriter, r *http.Request) {
|
||||
span := telemetry.SpanFromContext(r.Context())
|
||||
|
||||
name := r.URL.Query().Get("name")
|
||||
if !usernameRegex.MatchString(name) {
|
||||
err := fmt.Errorf("invalid username")
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := face.mapper.GetUserID(r.Context(), name)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("X-Anilist-User-Name", name)
|
||||
w.Header().Add("X-Anilist-User-Id", userId)
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintln(w, userId)
|
||||
}
|
41
cmd/handler/wire.go
Normal file
41
cmd/handler/wire.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wwmoraes/anilistarr/internal/adapters"
|
||||
"github.com/wwmoraes/anilistarr/internal/drivers/anilist"
|
||||
"github.com/wwmoraes/anilistarr/internal/drivers/caches"
|
||||
"github.com/wwmoraes/anilistarr/internal/drivers/providers"
|
||||
"github.com/wwmoraes/anilistarr/internal/drivers/stores"
|
||||
"github.com/wwmoraes/anilistarr/internal/usecases"
|
||||
)
|
||||
|
||||
func NewAnilistBridge(anilistEndpoint string, cacheOptions *caches.RedisOptions) (*usecases.MediaBridge, error) {
|
||||
tracker := anilist.New(anilistEndpoint, 50)
|
||||
if cacheOptions != nil {
|
||||
// cache, err := caches.NewRedis(cacheOptions)
|
||||
cache, err := caches.NewBolt("tmp/cache.db", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bolt cache initialization failed: %w", err)
|
||||
}
|
||||
|
||||
tracker, err = adapters.NewCachedTracker(tracker, cache)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cached adapter initialization failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
store, err := stores.NewSQL("sqlite", "tmp/media.db?loc=auto")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sql store initialization failed: %w", err)
|
||||
}
|
||||
|
||||
return &usecases.MediaBridge{
|
||||
Tracker: tracker,
|
||||
Mapper: &adapters.AnilistMapper{
|
||||
Source: providers.FribbsSource,
|
||||
Store: store,
|
||||
},
|
||||
}, nil
|
||||
}
|
172
cmd/version/main.go
Normal file
172
cmd/version/main.go
Normal file
|
@ -0,0 +1,172 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/mod/modfile"
|
||||
)
|
||||
|
||||
const templateString = `// Code generated by go generate. DO NOT EDIT.
|
||||
|
||||
package {{ .Package }}
|
||||
|
||||
const (
|
||||
{{ .Constants.Environment }} = "{{ .Environment }}"
|
||||
{{ .Constants.Module }} = "{{ .Module }}"
|
||||
{{ .Constants.Version }} = "{{ .Version }}"
|
||||
{{ .Constants.Name }} = "{{ .Name }}"
|
||||
{{ .Constants.Namespace }} = "{{ .Namespace }}"
|
||||
)`
|
||||
|
||||
type templateData struct {
|
||||
Environment string
|
||||
Module string
|
||||
Name string
|
||||
Namespace string
|
||||
Package string
|
||||
Version string
|
||||
Constants constantNames
|
||||
}
|
||||
|
||||
type constantNames struct {
|
||||
Environment string
|
||||
Module string
|
||||
Name string
|
||||
Namespace string
|
||||
Version string
|
||||
}
|
||||
|
||||
func main() {
|
||||
constEnvironment := flag.String(
|
||||
"const-environment",
|
||||
"ENVIRONMENT",
|
||||
"environment constant name",
|
||||
)
|
||||
constModule := flag.String(
|
||||
"const-module",
|
||||
"MODULE",
|
||||
"module constant name",
|
||||
)
|
||||
constName := flag.String(
|
||||
"const-name",
|
||||
"NAME",
|
||||
"application/service name constant name",
|
||||
)
|
||||
constNamespace := flag.String(
|
||||
"const-namespace",
|
||||
"NAMESPACE",
|
||||
"namespace constant name",
|
||||
)
|
||||
constVersion := flag.String(
|
||||
"const-version",
|
||||
"VERSION",
|
||||
"version constant name",
|
||||
)
|
||||
|
||||
environment := flag.String(
|
||||
"environment",
|
||||
"development",
|
||||
"target environment of the build",
|
||||
)
|
||||
modFile := flag.String(
|
||||
"mod",
|
||||
"go.mod",
|
||||
"Go mod file path",
|
||||
)
|
||||
name := flag.String(
|
||||
"name",
|
||||
"",
|
||||
"application/service name",
|
||||
)
|
||||
namespace := flag.String(
|
||||
"namespace",
|
||||
"",
|
||||
"namespace of the application (as per Open Telemetry definitions)",
|
||||
)
|
||||
packageName := flag.String(
|
||||
"package",
|
||||
"main",
|
||||
"name of the target package",
|
||||
)
|
||||
version := flag.String(
|
||||
"version",
|
||||
"",
|
||||
"version of the application",
|
||||
)
|
||||
|
||||
output := flag.String(
|
||||
"output",
|
||||
"",
|
||||
"output file to generate",
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
assert(checkFlag("name"))
|
||||
assert(checkFlag("namespace"))
|
||||
assert(checkFlag("version"))
|
||||
assert(checkFlag("output"))
|
||||
|
||||
constantsTemplate, err := template.New("").Parse(templateString)
|
||||
assert(err)
|
||||
|
||||
modData, err := os.ReadFile(*modFile)
|
||||
assert(err)
|
||||
|
||||
mod, err := modfile.Parse(*modFile, modData, nil)
|
||||
assert(err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = constantsTemplate.Execute(&buf, templateData{
|
||||
Environment: *environment,
|
||||
Module: mod.Module.Mod.Path,
|
||||
Name: *name,
|
||||
Namespace: *namespace,
|
||||
Package: *packageName,
|
||||
Version: *version,
|
||||
Constants: constantNames{
|
||||
Environment: *constEnvironment,
|
||||
Module: *constModule,
|
||||
Name: *constName,
|
||||
Namespace: *constNamespace,
|
||||
Version: *constVersion,
|
||||
},
|
||||
})
|
||||
assert(err)
|
||||
|
||||
data, err := format.Source(buf.Bytes())
|
||||
assert(err)
|
||||
|
||||
fd, err := os.Create(*output)
|
||||
assert(err)
|
||||
defer fd.Close()
|
||||
|
||||
_, err = fd.Write(data)
|
||||
assert(err)
|
||||
}
|
||||
|
||||
func assert(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func checkFlag(name string) error {
|
||||
f := flag.Lookup(name)
|
||||
if f == nil {
|
||||
return fmt.Errorf("flag %s not defined", name)
|
||||
}
|
||||
|
||||
if len(f.Value.String()) == 0 {
|
||||
return fmt.Errorf("flag %s must be set", name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
51
container-structure-test.yaml
Normal file
51
container-structure-test.yaml
Normal file
|
@ -0,0 +1,51 @@
|
|||
schemaVersion: "2.0.0"
|
||||
fileExistenceTests:
|
||||
- name: handler binary
|
||||
path: /usr/local/bin/handler
|
||||
shouldExist: true
|
||||
gid: 0
|
||||
uid: 0
|
||||
permissions: -rwxr-xr-x
|
||||
isExecutableBy: any
|
||||
|
||||
metadataTest:
|
||||
cmd: [/usr/local/bin/handler]
|
||||
workdir: /
|
||||
entrypoint: []
|
||||
exposedPorts:
|
||||
- "8080"
|
||||
labels:
|
||||
- key: org.opencontainers.image.authors
|
||||
value: ^([^<>]+? <[^@>]+@[^>]+>(, )?)+$
|
||||
isRegex: true
|
||||
- key: org.opencontainers.image.description
|
||||
value: .+
|
||||
isRegex: true
|
||||
- key: org.opencontainers.image.documentation
|
||||
value: https?://.+
|
||||
isRegex: true
|
||||
- key: org.opencontainers.image.licenses
|
||||
value: .+
|
||||
isRegex: true
|
||||
- key: org.opencontainers.image.source
|
||||
value: https?://.+
|
||||
isRegex: true
|
||||
- key: org.opencontainers.image.title
|
||||
value: .+
|
||||
isRegex: true
|
||||
- key: org.opencontainers.image.url
|
||||
value: https?://.+
|
||||
isRegex: true
|
||||
- key: org.opencontainers.image.vendor
|
||||
value: ^([^<>]+? <[^@>]+@[^>]+>(, )?)+$
|
||||
isRegex: true
|
||||
- key: org.opencontainers.image.version
|
||||
# yamllint disable-line rule:line-length
|
||||
value: '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'
|
||||
isRegex: true
|
||||
- key: org.opencontainers.image.created
|
||||
value: '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?Z$'
|
||||
isRegex: true
|
||||
- key: org.opencontainers.image.revision
|
||||
value: "^[0-9a-fA-F]{40}$"
|
||||
isRegex: true
|
19
fly.toml
Normal file
19
fly.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
# fly.toml app configuration file generated for anilistarr on 2023-07-27T00:01:05+02:00
|
||||
#
|
||||
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
|
||||
#
|
||||
|
||||
app = "anilistarr"
|
||||
primary_region = "ams"
|
||||
|
||||
[http_service]
|
||||
internal_port = 8080
|
||||
force_https = true
|
||||
auto_stop_machines = true
|
||||
auto_start_machines = true
|
||||
min_machines_running = 0
|
||||
processes = ["app"]
|
||||
|
||||
[metrics]
|
||||
port = 9091 # default for most prometheus clients
|
||||
path = "/metrics" # default for most prometheus clients
|
9
gen.go
Normal file
9
gen.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package anilistarr
|
||||
|
||||
//go:generate go run ./cmd/version/... -package telemetry -name handler -namespace media -version "$VERSION" -output internal/telemetry/constants.go
|
||||
|
||||
//// DISABLED: this generator needs a db to derive the code from
|
||||
// go:generate xo schema "file:tmp/media.db?loc=auto" -o internal/drivers/stores/models
|
||||
|
||||
//// TODO: genqlient generator
|
||||
//// cd internal/drivers/anilist && genqlient
|
66
go.mod
Normal file
66
go.mod
Normal file
|
@ -0,0 +1,66 @@
|
|||
module github.com/wwmoraes/anilistarr
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/Khan/genqlient v0.6.0
|
||||
github.com/MrAlias/otlpr v0.2.0
|
||||
github.com/XSAM/otelsql v0.23.0
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/go-logr/logr v1.2.4
|
||||
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5
|
||||
github.com/redis/go-redis/v9 v9.0.5
|
||||
go.etcd.io/bbolt v1.3.7
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0
|
||||
go.opentelemetry.io/otel v1.16.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.39.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0
|
||||
go.opentelemetry.io/otel/metric v1.16.0
|
||||
go.opentelemetry.io/otel/sdk v1.16.0
|
||||
go.opentelemetry.io/otel/sdk/metric v0.39.0
|
||||
go.opentelemetry.io/otel/trace v1.16.0
|
||||
golang.org/x/mod v0.10.0
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
|
||||
google.golang.org/grpc v1.57.0
|
||||
modernc.org/sqlite v1.24.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/vektah/gqlparser/v2 v2.5.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/sync v0.2.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
golang.org/x/tools v0.8.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
)
|
541
go.sum
Normal file
541
go.sum
Normal file
|
@ -0,0 +1,541 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Khan/genqlient v0.6.0 h1:Bwb1170ekuNIVIwTJEqvO8y7RxBxXu639VJOkKSrwAk=
|
||||
github.com/Khan/genqlient v0.6.0/go.mod h1:rvChwWVTqXhiapdhLDV4bp9tz/Xvtewwkon4DpWWCRM=
|
||||
github.com/MrAlias/otlpr v0.2.0 h1:dhf6kuadIhtzanrjluGnDH//po7S92FniNgyobgi6Mc=
|
||||
github.com/MrAlias/otlpr v0.2.0/go.mod h1:H+SQlbqgaFsTlKSNAxE+pGhHEZ/5SqY7uUW+Lt7H3RA=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/XSAM/otelsql v0.23.0 h1:NsJQS9YhI1+RDsFqE9mW5XIQmPmdF/qa8qQOLZN8XEA=
|
||||
github.com/XSAM/otelsql v0.23.0/go.mod h1:oX4LXMsb+9lAZhvHjUS61oQP/hbcJRadWHnBKNL+LuM=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
|
||||
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
|
||||
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
|
||||
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/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=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-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.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||
github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
|
||||
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=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/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=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
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/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
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/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho=
|
||||
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U=
|
||||
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc=
|
||||
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ=
|
||||
github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o=
|
||||
github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/vektah/gqlparser/v2 v2.5.1 h1:ZGu+bquAY23jsxDRcYpWjttRZrUz07LbiY77gUOHcr4=
|
||||
github.com/vektah/gqlparser/v2 v2.5.1/go.mod h1:mPgqFBu/woKTVYWyNk8cO3kh4S/f4aRFZrvOnp3hmCs=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
||||
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0/go.mod h1:XiYsayHc36K3EByOO6nbAXnAWbrUxdjUROCEeeROOH8=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0 h1:EbmAUG9hEAMXyfWEasIt2kmh/WmXUznUksChApTgBGc=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0/go.mod h1:rD9feqRYP24P14t5kmhNMqsqm1jvKmpx2H2rKVw52V8=
|
||||
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
|
||||
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 h1:t4ZwRPU+emrcvM2e9DHd0Fsf0JTPVcbfa/BhTDF03d0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0/go.mod h1:vLarbg68dH2Wa77g71zmKQqlQ8+8Rq3GRG31uc0WcWI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.39.0 h1:f6BwB2OACc3FCbYVznctQ9V6KK7Vq6CjmYXJ7DeSs4E=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.39.0/go.mod h1:UqL5mZ3qs6XYhDnZaW1Ps4upD+PX6LipH40AoeuIlwU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.39.0 h1:rm+Fizi7lTM2UefJ1TO347fSRcwmIsUAaZmYmIGBRAo=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.39.0/go.mod h1:sWFbI3jJ+6JdjOVepA5blpv/TJ20Hw+26561iMbWcwU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 h1:cbsD4cUcviQGXdw8+bo5x2wazq10SKz8hEbtCRPcU78=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0/go.mod h1:JgXSGah17croqhJfhByOLVY719k1emAXC8MVhCIJlRs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 h1:TVQp/bboR4mhZSav+MdgXB8FaRho1RC8UwVn3T0vjVc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0/go.mod h1:I33vtIe0sR96wfrUcilIzLoA3mLHhRmz9S9Te0S3gDo=
|
||||
go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
|
||||
go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
|
||||
go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE=
|
||||
go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
|
||||
go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI=
|
||||
go.opentelemetry.io/otel/sdk/metric v0.39.0/go.mod h1:piDIRgjcK7u0HCL5pCA4e74qpK/jk3NiUoAHATVAmiI=
|
||||
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
|
||||
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw=
|
||||
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/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-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=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
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=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/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-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=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
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=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 h1:9NWlQfY2ePejTmfwUH1OWwmznFa+0kKcHGPDvcPza9M=
|
||||
google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 h1:m8v1xLLLzMe1m5P+gCTF8nJB9epwZQUBERm20Oy1poQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
|
||||
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
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.27.1/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=
|
||||
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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/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.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
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.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.24.0 h1:EsClRIWHGhLTCX44p+Ri/JLD+vFGo0QGjasg2/F9TlI=
|
||||
modernc.org/sqlite v1.24.0/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
||||
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
|
||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
90
internal/adapters/anilistmapper.go
Normal file
90
internal/adapters/anilistmapper.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/wwmoraes/anilistarr/internal/entities"
|
||||
"github.com/wwmoraes/anilistarr/internal/telemetry"
|
||||
)
|
||||
|
||||
type AnilistMapper struct {
|
||||
Source MetadataSource[Metadata]
|
||||
Store AnilistStore
|
||||
}
|
||||
|
||||
func (mapper *AnilistMapper) Close() error {
|
||||
return mapper.Store.Close()
|
||||
}
|
||||
|
||||
func (mapper *AnilistMapper) MapID(ctx context.Context, anilistId string) (string, error) {
|
||||
ctx, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
media, err := mapper.Store.MappingByAnilistID(ctx, anilistId)
|
||||
|
||||
if err != nil {
|
||||
return "", span.Assert(fmt.Errorf("failed to map ID %s: %w", anilistId, err))
|
||||
}
|
||||
|
||||
if media == nil {
|
||||
return "", span.Assert(nil)
|
||||
}
|
||||
|
||||
return media.TvdbID, span.Assert(nil)
|
||||
}
|
||||
|
||||
func (mapper *AnilistMapper) MapIDs(ctx context.Context, anilistIds []string) ([]string, error) {
|
||||
ctx, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
records, err := mapper.Store.MappingByAnilistIDBulk(ctx, anilistIds)
|
||||
if err != nil {
|
||||
return nil, span.Assert(fmt.Errorf("failed to map IDs: %w", err))
|
||||
}
|
||||
|
||||
ids := make([]string, len(records))
|
||||
for index, record := range records {
|
||||
ids[index] = record.TvdbID
|
||||
}
|
||||
|
||||
// for _, sourceId := range anilistIds {
|
||||
// targetId, err := mapper.MapID(ctx, sourceId)
|
||||
// if err != nil {
|
||||
// return nil, span.Assert(fmt.Errorf("failed to map IDs: %w", err))
|
||||
// }
|
||||
|
||||
// ids = append(ids, targetId)
|
||||
// }
|
||||
|
||||
return ids, span.Assert(nil)
|
||||
}
|
||||
|
||||
func (mapper *AnilistMapper) Refresh(ctx context.Context) error {
|
||||
ctx, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
data, err := mapper.Source.Fetch(ctx, nil)
|
||||
if err != nil {
|
||||
return span.Assert(fmt.Errorf("failed to refresh anilist mapper: %w", err))
|
||||
}
|
||||
|
||||
medias := make([]*entities.Media, 0, len(data))
|
||||
for _, entry := range data {
|
||||
if entry.GetTvdbID() == "0" || entry.GetAnilistID() == "0" {
|
||||
continue
|
||||
}
|
||||
|
||||
medias = append(medias, &entities.Media{
|
||||
AnilistID: entry.GetAnilistID(),
|
||||
TvdbID: entry.GetTvdbID(),
|
||||
})
|
||||
}
|
||||
|
||||
err = mapper.Store.PutMediaBulk(ctx, medias)
|
||||
if err != nil {
|
||||
return span.Assert(fmt.Errorf("failed to store media during refresh: %w", err))
|
||||
}
|
||||
|
||||
return span.Assert(nil)
|
||||
}
|
13
internal/adapters/cache.go
Normal file
13
internal/adapters/cache.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Cache interface {
|
||||
io.Closer
|
||||
|
||||
GetString(ctx context.Context, key string) (string, error)
|
||||
SetString(ctx context.Context, key, value string) error
|
||||
}
|
64
internal/adapters/cachedtracker.go
Normal file
64
internal/adapters/cachedtracker.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/wwmoraes/anilistarr/internal/telemetry"
|
||||
"github.com/wwmoraes/anilistarr/internal/usecases"
|
||||
)
|
||||
|
||||
const (
|
||||
cacheKeyUserID string = "anilist:user:%s:id"
|
||||
)
|
||||
|
||||
type cachedTracker struct {
|
||||
cache Cache
|
||||
tracker usecases.Tracker
|
||||
}
|
||||
|
||||
func NewCachedTracker(tracker usecases.Tracker, cache Cache) (usecases.Tracker, error) {
|
||||
return &cachedTracker{
|
||||
cache: cache,
|
||||
tracker: tracker,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (wrapper *cachedTracker) GetUserID(ctx context.Context, name string) (string, error) {
|
||||
ctx, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(cacheKeyUserID, name)
|
||||
|
||||
span.AddEvent("try cache")
|
||||
userId, err := wrapper.cache.GetString(ctx, key)
|
||||
if err != nil {
|
||||
return "", span.Assert(fmt.Errorf("failed to get user ID: %w", err))
|
||||
}
|
||||
|
||||
if userId != "" {
|
||||
span.AddEvent("cache hit")
|
||||
return userId, span.Assert(err)
|
||||
}
|
||||
|
||||
span.AddEvent("cache miss")
|
||||
userId, err = wrapper.tracker.GetUserID(ctx, name)
|
||||
if err != nil {
|
||||
return "", span.Assert(fmt.Errorf("failed to get user ID: %w", err))
|
||||
}
|
||||
|
||||
return userId, span.Assert(wrapper.cache.SetString(ctx, key, userId))
|
||||
}
|
||||
|
||||
func (wrapper *cachedTracker) GetMediaListIDs(ctx context.Context, userId string) ([]string, error) {
|
||||
ctx, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
ids, err := wrapper.tracker.GetMediaListIDs(ctx, userId)
|
||||
|
||||
return ids, span.Assert(err)
|
||||
}
|
||||
|
||||
func (wrapper *cachedTracker) Close() error {
|
||||
return wrapper.cache.Close()
|
||||
}
|
25
internal/adapters/jsonlocalpath.go
Normal file
25
internal/adapters/jsonlocalpath.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/wwmoraes/anilistarr/internal/telemetry"
|
||||
)
|
||||
|
||||
type JSONLocalPath[F Metadata] string
|
||||
|
||||
func (source JSONLocalPath[F]) Fetch(ctx context.Context) ([]Metadata, error) {
|
||||
_, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
data, err := os.ReadFile(string(source))
|
||||
if err != nil {
|
||||
return nil, span.Assert(fmt.Errorf("failed to read local JSON: %w", err))
|
||||
}
|
||||
|
||||
metadata, err := unmarshalJSON[F](data)
|
||||
|
||||
return metadata, span.Assert(err)
|
||||
}
|
36
internal/adapters/jsonsourceurl.go
Normal file
36
internal/adapters/jsonsourceurl.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/wwmoraes/anilistarr/internal/telemetry"
|
||||
)
|
||||
|
||||
type JSONSourceURL[F Metadata] string
|
||||
|
||||
func (source JSONSourceURL[F]) Fetch(ctx context.Context, client Getter) ([]Metadata, error) {
|
||||
_, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
|
||||
res, err := client.Get(string(source))
|
||||
if err != nil {
|
||||
return nil, span.Assert(fmt.Errorf("failed to fetch remote JSON: %w", err))
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, span.Assert(fmt.Errorf("failed to read fetched JSON response body: %w", err))
|
||||
}
|
||||
|
||||
metadata, err := unmarshalJSON[F](data)
|
||||
|
||||
return metadata, span.Assert(err)
|
||||
}
|
86
internal/adapters/jsonsourceurl_test.go
Normal file
86
internal/adapters/jsonsourceurl_test.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package adapters_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/wwmoraes/anilistarr/internal/adapters"
|
||||
)
|
||||
|
||||
type mockClient struct {
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func (client *mockClient) Get(url string) (*http.Response, error) {
|
||||
data, ok := client.data[url]
|
||||
if !ok {
|
||||
return &http.Response{
|
||||
Status: http.StatusText(http.StatusNotFound),
|
||||
StatusCode: http.StatusNotFound,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &http.Response{
|
||||
Status: http.StatusText(http.StatusOK),
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewBufferString(data)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type mockData struct {
|
||||
AnilistID string `json:"anilist_id,omitempty"`
|
||||
TvdbID string `json:"tvdb_id,omitempty"`
|
||||
}
|
||||
|
||||
func (data mockData) GetAnilistID() string {
|
||||
return data.AnilistID
|
||||
}
|
||||
|
||||
func (data mockData) GetTvdbID() string {
|
||||
return data.TvdbID
|
||||
}
|
||||
|
||||
func TestJSONSourceURL_Fetch(t *testing.T) {
|
||||
expectedData := []adapters.Metadata{
|
||||
mockData{
|
||||
AnilistID: "123",
|
||||
TvdbID: "456",
|
||||
},
|
||||
}
|
||||
|
||||
bytesData, err := json.Marshal(expectedData)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
provider := adapters.JSONSourceURL[mockData]("/anime-lists.json")
|
||||
|
||||
metadata, err := provider.Fetch(ctx, &mockClient{
|
||||
data: map[string]string{
|
||||
"/anime-lists.json": string(bytesData),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if len(metadata) != len(expectedData) {
|
||||
t.Errorf("metadata length mismatch: got %d, wanted %d", len(metadata), len(expectedData))
|
||||
}
|
||||
|
||||
for index, entry := range metadata {
|
||||
if entry.GetAnilistID() != expectedData[index].GetAnilistID() {
|
||||
t.Errorf("metadata anilist ID mismatch: got %s, expected %s", entry.GetAnilistID(), expectedData[index].GetAnilistID())
|
||||
}
|
||||
|
||||
if entry.GetTvdbID() != expectedData[index].GetTvdbID() {
|
||||
t.Errorf("metadata tvdb ID mismatch: got %s, expected %s", entry.GetTvdbID(), expectedData[index].GetTvdbID())
|
||||
}
|
||||
}
|
||||
}
|
26
internal/adapters/metadata.go
Normal file
26
internal/adapters/metadata.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package adapters
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Metadata interface {
|
||||
GetAnilistID() string
|
||||
GetTvdbID() string
|
||||
}
|
||||
|
||||
func unmarshalJSON[F Metadata](data []byte) ([]Metadata, error) {
|
||||
var dataEntries []F
|
||||
err := json.Unmarshal(data, &dataEntries)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
|
||||
}
|
||||
|
||||
entries := make([]Metadata, len(dataEntries))
|
||||
for index, entry := range dataEntries {
|
||||
entries[index] = entry
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
14
internal/adapters/metasource.go
Normal file
14
internal/adapters/metasource.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Getter interface {
|
||||
Get(string) (*http.Response, error)
|
||||
}
|
||||
|
||||
type MetadataSource[T Metadata] interface {
|
||||
Fetch(ctx context.Context, client Getter) ([]T, error)
|
||||
}
|
22
internal/adapters/store.go
Normal file
22
internal/adapters/store.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/wwmoraes/anilistarr/internal/entities"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
io.Closer
|
||||
|
||||
PutMedia(ctx context.Context, media *entities.Media) error
|
||||
PutMediaBulk(ctx context.Context, medias []*entities.Media) error
|
||||
}
|
||||
|
||||
type AnilistStore interface {
|
||||
Store
|
||||
|
||||
MappingByAnilistID(ctx context.Context, anilistId string) (*entities.Media, error)
|
||||
MappingByAnilistIDBulk(ctx context.Context, anilistIds []string) ([]*entities.Media, error)
|
||||
}
|
101
internal/drivers/anilist/anilist.go
Normal file
101
internal/drivers/anilist/anilist.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
package anilist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/Khan/genqlient/graphql"
|
||||
"github.com/wwmoraes/anilistarr/internal/telemetry"
|
||||
"github.com/wwmoraes/anilistarr/internal/usecases"
|
||||
)
|
||||
|
||||
const (
|
||||
// interval reflects the current Anilist API rate limits
|
||||
// https://anilist.gitbook.io/anilist-apiv2-docs/overview/rate-limiting
|
||||
interval time.Duration = time.Minute
|
||||
// requests reflects the current Anilist API rate limits
|
||||
// https://anilist.gitbook.io/anilist-apiv2-docs/overview/rate-limiting
|
||||
requests int = 90
|
||||
)
|
||||
|
||||
type Tracker struct {
|
||||
Client graphql.Client
|
||||
PageSize int
|
||||
}
|
||||
|
||||
func New(anilistEndpoint string, pageSize int) usecases.Tracker {
|
||||
return &Tracker{
|
||||
Client: NewGraphQLClient(anilistEndpoint),
|
||||
PageSize: pageSize,
|
||||
}
|
||||
}
|
||||
|
||||
func NewGraphQLClient(anilistEndpoint string) graphql.Client {
|
||||
return graphql.NewClient(anilistEndpoint, NewRatedClient(interval, requests, nil))
|
||||
}
|
||||
|
||||
func (tracker *Tracker) GetUserID(ctx context.Context, name string) (string, error) {
|
||||
ctx, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
res, err := GetUserByName(ctx, tracker.Client, name)
|
||||
if err != nil {
|
||||
return "", span.Assert(fmt.Errorf("failed to get user by name: %w", err))
|
||||
}
|
||||
|
||||
return strconv.Itoa(res.User.Id), span.Assert(nil)
|
||||
}
|
||||
|
||||
func (tracker *Tracker) GetMediaListIDs(ctx context.Context, userId string) ([]string, error) {
|
||||
log := telemetry.LoggerFromContext(ctx)
|
||||
ctx, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
userIdInt, err := strconv.Atoi(userId)
|
||||
if err != nil {
|
||||
return nil, span.Assert(fmt.Errorf("failed to convert user ID to integer: %w", err))
|
||||
}
|
||||
|
||||
page := 1
|
||||
anilistIds := make([]string, 0, tracker.PageSize)
|
||||
telemetry.Int(span, "page.size", tracker.PageSize)
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
|
||||
log.Info("requesting media list", "page", page)
|
||||
extCtx, extSpan := telemetry.Start(
|
||||
ctx,
|
||||
"anilist.GetWatching",
|
||||
telemetry.WithSpanKindClient(),
|
||||
telemetry.WithInt("page", page),
|
||||
)
|
||||
res, err := GetWatching(extCtx, tracker.Client, userIdInt, page, tracker.PageSize)
|
||||
err = extSpan.Assert(err)
|
||||
extSpan.End()
|
||||
if err != nil {
|
||||
return nil, span.Assert(fmt.Errorf("failed to fetch media list: %w", err))
|
||||
}
|
||||
|
||||
if len(res.Page.MediaList) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// far from optimal, I know, yet it works fine unless the use has thousands
|
||||
// of entries...
|
||||
for _, entry := range res.Page.MediaList {
|
||||
anilistIds = append(anilistIds, strconv.Itoa(entry.Media.Id))
|
||||
}
|
||||
|
||||
page++
|
||||
}
|
||||
|
||||
return anilistIds, span.Assert(nil)
|
||||
}
|
||||
|
||||
func (tracker *Tracker) Close() error {
|
||||
return nil
|
||||
}
|
200
internal/drivers/anilist/generated.go
Normal file
200
internal/drivers/anilist/generated.go
Normal file
|
@ -0,0 +1,200 @@
|
|||
// Code generated by github.com/Khan/genqlient, DO NOT EDIT.
|
||||
|
||||
package anilist
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/Khan/genqlient/graphql"
|
||||
)
|
||||
|
||||
// GetUserByNameResponse is returned by GetUserByName on success.
|
||||
type GetUserByNameResponse struct {
|
||||
// User query
|
||||
User GetUserByNameUser `json:"User"`
|
||||
}
|
||||
|
||||
// GetUser returns GetUserByNameResponse.User, and is useful for accessing the field via an interface.
|
||||
func (v *GetUserByNameResponse) GetUser() GetUserByNameUser { return v.User }
|
||||
|
||||
// GetUserByNameUser includes the requested fields of the GraphQL type User.
|
||||
// The GraphQL type's documentation follows.
|
||||
//
|
||||
// A user
|
||||
type GetUserByNameUser struct {
|
||||
// The id of the user
|
||||
Id int `json:"id"`
|
||||
}
|
||||
|
||||
// GetId returns GetUserByNameUser.Id, and is useful for accessing the field via an interface.
|
||||
func (v *GetUserByNameUser) GetId() int { return v.Id }
|
||||
|
||||
// GetWatchingPage includes the requested fields of the GraphQL type Page.
|
||||
// The GraphQL type's documentation follows.
|
||||
//
|
||||
// Page of data
|
||||
type GetWatchingPage struct {
|
||||
MediaList []GetWatchingPageMediaList `json:"mediaList"`
|
||||
}
|
||||
|
||||
// GetMediaList returns GetWatchingPage.MediaList, and is useful for accessing the field via an interface.
|
||||
func (v *GetWatchingPage) GetMediaList() []GetWatchingPageMediaList { return v.MediaList }
|
||||
|
||||
// GetWatchingPageMediaList includes the requested fields of the GraphQL type MediaList.
|
||||
// The GraphQL type's documentation follows.
|
||||
//
|
||||
// List of anime or manga
|
||||
type GetWatchingPageMediaList struct {
|
||||
Media GetWatchingPageMediaListMedia `json:"media"`
|
||||
}
|
||||
|
||||
// GetMedia returns GetWatchingPageMediaList.Media, and is useful for accessing the field via an interface.
|
||||
func (v *GetWatchingPageMediaList) GetMedia() GetWatchingPageMediaListMedia { return v.Media }
|
||||
|
||||
// GetWatchingPageMediaListMedia includes the requested fields of the GraphQL type Media.
|
||||
// The GraphQL type's documentation follows.
|
||||
//
|
||||
// Anime or Manga
|
||||
type GetWatchingPageMediaListMedia struct {
|
||||
// The id of the media
|
||||
Id int `json:"id"`
|
||||
// The mal id of the media
|
||||
IdMal int `json:"idMal"`
|
||||
// The official titles of the media in various languages
|
||||
Title GetWatchingPageMediaListMediaTitle `json:"title"`
|
||||
}
|
||||
|
||||
// GetId returns GetWatchingPageMediaListMedia.Id, and is useful for accessing the field via an interface.
|
||||
func (v *GetWatchingPageMediaListMedia) GetId() int { return v.Id }
|
||||
|
||||
// GetIdMal returns GetWatchingPageMediaListMedia.IdMal, and is useful for accessing the field via an interface.
|
||||
func (v *GetWatchingPageMediaListMedia) GetIdMal() int { return v.IdMal }
|
||||
|
||||
// GetTitle returns GetWatchingPageMediaListMedia.Title, and is useful for accessing the field via an interface.
|
||||
func (v *GetWatchingPageMediaListMedia) GetTitle() GetWatchingPageMediaListMediaTitle { return v.Title }
|
||||
|
||||
// GetWatchingPageMediaListMediaTitle includes the requested fields of the GraphQL type MediaTitle.
|
||||
// The GraphQL type's documentation follows.
|
||||
//
|
||||
// The official titles of the media in various languages
|
||||
type GetWatchingPageMediaListMediaTitle struct {
|
||||
// The romanization of the native language title
|
||||
Romaji string `json:"romaji"`
|
||||
}
|
||||
|
||||
// GetRomaji returns GetWatchingPageMediaListMediaTitle.Romaji, and is useful for accessing the field via an interface.
|
||||
func (v *GetWatchingPageMediaListMediaTitle) GetRomaji() string { return v.Romaji }
|
||||
|
||||
// GetWatchingResponse is returned by GetWatching on success.
|
||||
type GetWatchingResponse struct {
|
||||
Page GetWatchingPage `json:"Page"`
|
||||
}
|
||||
|
||||
// GetPage returns GetWatchingResponse.Page, and is useful for accessing the field via an interface.
|
||||
func (v *GetWatchingResponse) GetPage() GetWatchingPage { return v.Page }
|
||||
|
||||
// __GetUserByNameInput is used internally by genqlient
|
||||
type __GetUserByNameInput struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// GetName returns __GetUserByNameInput.Name, and is useful for accessing the field via an interface.
|
||||
func (v *__GetUserByNameInput) GetName() string { return v.Name }
|
||||
|
||||
// __GetWatchingInput is used internally by genqlient
|
||||
type __GetWatchingInput struct {
|
||||
UserId int `json:"userId"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"perPage"`
|
||||
}
|
||||
|
||||
// GetUserId returns __GetWatchingInput.UserId, and is useful for accessing the field via an interface.
|
||||
func (v *__GetWatchingInput) GetUserId() int { return v.UserId }
|
||||
|
||||
// GetPage returns __GetWatchingInput.Page, and is useful for accessing the field via an interface.
|
||||
func (v *__GetWatchingInput) GetPage() int { return v.Page }
|
||||
|
||||
// GetPerPage returns __GetWatchingInput.PerPage, and is useful for accessing the field via an interface.
|
||||
func (v *__GetWatchingInput) GetPerPage() int { return v.PerPage }
|
||||
|
||||
// The query or mutation executed by GetUserByName.
|
||||
const GetUserByName_Operation = `
|
||||
query GetUserByName ($name: String!) {
|
||||
User(name: $name) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func GetUserByName(
|
||||
ctx context.Context,
|
||||
client graphql.Client,
|
||||
name string,
|
||||
) (*GetUserByNameResponse, error) {
|
||||
req := &graphql.Request{
|
||||
OpName: "GetUserByName",
|
||||
Query: GetUserByName_Operation,
|
||||
Variables: &__GetUserByNameInput{
|
||||
Name: name,
|
||||
},
|
||||
}
|
||||
var err error
|
||||
|
||||
var data GetUserByNameResponse
|
||||
resp := &graphql.Response{Data: &data}
|
||||
|
||||
err = client.MakeRequest(
|
||||
ctx,
|
||||
req,
|
||||
resp,
|
||||
)
|
||||
|
||||
return &data, err
|
||||
}
|
||||
|
||||
// The query or mutation executed by GetWatching.
|
||||
const GetWatching_Operation = `
|
||||
query GetWatching ($userId: Int!, $page: Int!, $perPage: Int!) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
mediaList(userId: $userId, type: ANIME, status: CURRENT) {
|
||||
media {
|
||||
id
|
||||
idMal
|
||||
title {
|
||||
romaji
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func GetWatching(
|
||||
ctx context.Context,
|
||||
client graphql.Client,
|
||||
userId int,
|
||||
page int,
|
||||
perPage int,
|
||||
) (*GetWatchingResponse, error) {
|
||||
req := &graphql.Request{
|
||||
OpName: "GetWatching",
|
||||
Query: GetWatching_Operation,
|
||||
Variables: &__GetWatchingInput{
|
||||
UserId: userId,
|
||||
Page: page,
|
||||
PerPage: perPage,
|
||||
},
|
||||
}
|
||||
var err error
|
||||
|
||||
var data GetWatchingResponse
|
||||
resp := &graphql.Response{Data: &data}
|
||||
|
||||
err = client.MakeRequest(
|
||||
ctx,
|
||||
req,
|
||||
resp,
|
||||
)
|
||||
|
||||
return &data, err
|
||||
}
|
6
internal/drivers/anilist/genqlient.yaml
Normal file
6
internal/drivers/anilist/genqlient.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Default genqlient config; for full documentation see:
|
||||
# https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml
|
||||
schema: schema.graphql
|
||||
operations:
|
||||
- queries.graphql
|
||||
generated: generated.go
|
91
internal/drivers/anilist/httpclient.go
Normal file
91
internal/drivers/anilist/httpclient.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package anilist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/Khan/genqlient/graphql"
|
||||
"github.com/wwmoraes/anilistarr/internal/telemetry"
|
||||
"go.opentelemetry.io/otel/semconv/v1.20.0/httpconv"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type ratedClient struct {
|
||||
client *http.Client
|
||||
rater *rate.Limiter
|
||||
}
|
||||
|
||||
func NewRatedClient(interval time.Duration, requests int, base *http.Client) graphql.Doer {
|
||||
if base == nil {
|
||||
base = http.DefaultClient
|
||||
}
|
||||
|
||||
return &ratedClient{
|
||||
client: base,
|
||||
rater: rate.NewLimiter(rate.Every(interval), requests),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO check if we can update the rate limiter safely (without resetting the current count)
|
||||
func (c *ratedClient) Do(req *http.Request) (*http.Response, error) {
|
||||
span := telemetry.SpanFromContext(req.Context())
|
||||
err := c.rater.Wait(req.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
span.SetAttributes(httpconv.ResponseHeader(telemetry.WantedRequestHeaders(
|
||||
resp.Header,
|
||||
"X-RateLimit-Remaining",
|
||||
"X-RateLimit-Limit",
|
||||
"Retry-After",
|
||||
))...)
|
||||
|
||||
if resp.StatusCode != http.StatusTooManyRequests {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
//// exceptional case: we wait and retry
|
||||
//// first we make sure the rate limiter has the right burst
|
||||
remaining, err := strconv.Atoi(resp.Header.Get("X-RateLimit-Remaining"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.rater.SetBurst(remaining)
|
||||
|
||||
//// this should never happen if the rate limiter is properly set and the API
|
||||
//// lives by its documentation
|
||||
if remaining != 0 {
|
||||
return nil, fmt.Errorf("WARNING inconsistent upstream API - try increasing the rate interval")
|
||||
}
|
||||
|
||||
//// update future bursts
|
||||
burst, err := strconv.Atoi(resp.Header.Get("X-RateLimit-Limit"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reset, err := strconv.ParseInt(resp.Header.Get("X-RateLimit-Reset"), 10, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.rater.SetBurstAt(time.Unix(reset, 0), burst)
|
||||
|
||||
//// respect the wait time proposed by the API
|
||||
after, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(after) * time.Second)
|
||||
|
||||
return c.Do(req)
|
||||
}
|
19
internal/drivers/anilist/queries.graphql
Normal file
19
internal/drivers/anilist/queries.graphql
Normal file
|
@ -0,0 +1,19 @@
|
|||
query GetUserByName($name:String!){
|
||||
User(name: $name) {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
query GetWatching($userId: Int!, $page: Int!, $perPage:Int!) {
|
||||
Page(page:$page, perPage: $perPage) {
|
||||
mediaList(userId: $userId, type: ANIME, status: CURRENT) {
|
||||
media {
|
||||
id
|
||||
idMal
|
||||
title {
|
||||
romaji
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10025
internal/drivers/anilist/schema.graphql
Normal file
10025
internal/drivers/anilist/schema.graphql
Normal file
File diff suppressed because it is too large
Load diff
76
internal/drivers/caches/bolt.go
Normal file
76
internal/drivers/caches/bolt.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package caches
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/wwmoraes/anilistarr/internal/adapters"
|
||||
"github.com/wwmoraes/anilistarr/internal/telemetry"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
const bucketName = "anilistarr"
|
||||
|
||||
type BoltOptions = bbolt.Options
|
||||
|
||||
type boltCache struct {
|
||||
*bbolt.DB
|
||||
}
|
||||
|
||||
func NewBolt(path string, options *BoltOptions) (adapters.Cache, error) {
|
||||
db, err := bbolt.Open(path, 0640, options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open bolt database: %w", err)
|
||||
}
|
||||
|
||||
err = db.Update(func(tx *bbolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(bucketName))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create/get bucket: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize bolt: %w", err)
|
||||
}
|
||||
|
||||
return &boltCache{db}, nil
|
||||
}
|
||||
|
||||
func (c *boltCache) GetString(ctx context.Context, key string) (string, error) {
|
||||
_, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
var value string
|
||||
|
||||
err := c.View(func(tx *bbolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
if bucket == nil {
|
||||
return fmt.Errorf("bucket %s does not exist", bucketName)
|
||||
}
|
||||
|
||||
data := bucket.Get([]byte(key))
|
||||
value = string(data)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", span.Assert(fmt.Errorf("failed to get string: %w", err))
|
||||
}
|
||||
|
||||
return value, span.Assert(nil)
|
||||
}
|
||||
|
||||
func (c *boltCache) SetString(ctx context.Context, key, value string) error {
|
||||
_, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
return span.Assert(c.Update(func(tx *bbolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
if bucket == nil {
|
||||
return fmt.Errorf("bucket %s does not exist", bucketName)
|
||||
}
|
||||
|
||||
return bucket.Put([]byte(key), []byte(value))
|
||||
}))
|
||||
}
|
56
internal/drivers/caches/redis.go
Normal file
56
internal/drivers/caches/redis.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package caches
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/redis/go-redis/extra/redisotel/v9"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/wwmoraes/anilistarr/internal/adapters"
|
||||
"github.com/wwmoraes/anilistarr/internal/telemetry"
|
||||
)
|
||||
|
||||
type RedisOptions = redis.Options
|
||||
|
||||
func NewRedis(options *RedisOptions) (adapters.Cache, error) {
|
||||
rdb := redis.NewClient(options)
|
||||
|
||||
err := redisotel.InstrumentTracing(rdb)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to instrument tracing for Redis: %w", err)
|
||||
}
|
||||
|
||||
err = redisotel.InstrumentMetrics(rdb)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to instrument metrics for Redis: %w", err)
|
||||
}
|
||||
|
||||
return &redisCache{rdb}, nil
|
||||
}
|
||||
|
||||
type redisCache struct {
|
||||
*redis.Client
|
||||
}
|
||||
|
||||
func (c *redisCache) GetString(ctx context.Context, key string) (string, error) {
|
||||
ctx, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
res, err := c.Get(ctx, key).Result()
|
||||
if err == redis.Nil {
|
||||
return "", span.Assert(nil)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", span.Assert(fmt.Errorf("failed to get string: %w", err))
|
||||
}
|
||||
|
||||
return res, span.Assert(nil)
|
||||
}
|
||||
|
||||
func (c *redisCache) SetString(ctx context.Context, key, value string) error {
|
||||
ctx, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
return span.Assert(c.Set(ctx, key, value, 0).Err())
|
||||
}
|
38
internal/drivers/providers/fribbs.go
Normal file
38
internal/drivers/providers/fribbs.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package providers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/wwmoraes/anilistarr/internal/adapters"
|
||||
)
|
||||
|
||||
const FribbsSource adapters.JSONSourceURL[FribbsEntry] = "https://github.com/Fribb/anime-lists/raw/master/anime-list-full.json"
|
||||
|
||||
type FribbsEntry struct {
|
||||
AnilistID uint64 `json:"anilist_id,omitempty"`
|
||||
TvdbID uint64 `json:"thetvdb_id,omitempty"`
|
||||
|
||||
//// useless
|
||||
// Type string `json:"type,omitempty"`
|
||||
|
||||
//// commented out as we don't need these
|
||||
// AnidbID uint `json:"anidb_id,omitempty"`
|
||||
// AnisearchID uint `json:"anisearch_id,omitempty"`
|
||||
// ImdbID string `json:"imdb_id,omitempty"`
|
||||
// KitsuID uint `json:"kitsu_id,omitempty"`
|
||||
// LivechartID uint `json:"livechart_id,omitempty"`
|
||||
// MalID uint `json:"mal_id,omitempty"`
|
||||
// NotifyMoeID string `json:"notify.moe_id,omitempty"`
|
||||
|
||||
//// those are even worse as they mix strings and numbers
|
||||
// AnimePlanetID string `json:"anime-planet_id,omitempty"`
|
||||
// TmdbID uint `json:"themoviedb_id,omitempty"`
|
||||
}
|
||||
|
||||
func (entry FribbsEntry) GetTvdbID() string {
|
||||
return strconv.FormatUint(entry.TvdbID, 10)
|
||||
}
|
||||
|
||||
func (entry FribbsEntry) GetAnilistID() string {
|
||||
return strconv.FormatUint(entry.AnilistID, 10)
|
||||
}
|
92
internal/drivers/stores/models/bulk.go
Normal file
92
internal/drivers/stores/models/bulk.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type MappingList []*Mapping
|
||||
|
||||
func (m MappingList) Upsert(ctx context.Context, db DB) error {
|
||||
rows := make([]string, len(m))
|
||||
for index, entry := range m {
|
||||
if entry._deleted {
|
||||
return logerror(&ErrUpsertFailed{ErrMarkedForDeletion})
|
||||
}
|
||||
|
||||
rows[index] = fmt.Sprintf("(%s, %s)", entry.TvdbID, nullableString(entry.AnilistID))
|
||||
}
|
||||
|
||||
const baseSqlstr = `INSERT INTO mapping (` +
|
||||
`tvdb_id, anilist_id` +
|
||||
`) VALUES %s` +
|
||||
` ON CONFLICT (tvdb_id) DO ` +
|
||||
`UPDATE SET ` +
|
||||
`anilist_id = EXCLUDED.anilist_id `
|
||||
|
||||
sqlstr := fmt.Sprintf(baseSqlstr, strings.Join(rows, ","))
|
||||
|
||||
logf(sqlstr)
|
||||
if _, err := db.ExecContext(ctx, sqlstr); err != nil {
|
||||
return logerror(err)
|
||||
}
|
||||
|
||||
for _, entry := range m {
|
||||
entry._exists = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func MappingByAnilistIDBulk(ctx context.Context, db DB, anilistIDs []sql.NullString) ([]*Mapping, error) {
|
||||
ids := make([]string, 0, len(anilistIDs))
|
||||
for _, id := range anilistIDs {
|
||||
if !id.Valid {
|
||||
continue
|
||||
}
|
||||
|
||||
ids = append(ids, id.String)
|
||||
}
|
||||
|
||||
// query
|
||||
const baseSqlstr = `SELECT ` +
|
||||
`tvdb_id, anilist_id ` +
|
||||
`FROM mapping ` +
|
||||
`WHERE anilist_id IN (%s)`
|
||||
sqlstr := fmt.Sprintf(baseSqlstr, strings.Join(ids, ","))
|
||||
|
||||
// run
|
||||
logf(sqlstr)
|
||||
rows, err := db.QueryContext(ctx, sqlstr)
|
||||
if err != nil {
|
||||
return nil, logerror(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
results := make([]*Mapping, 0, len(ids))
|
||||
var m *Mapping
|
||||
for rows.Next() {
|
||||
m = &Mapping{
|
||||
_exists: true,
|
||||
}
|
||||
|
||||
err = rows.Scan(&m.TvdbID, &m.AnilistID)
|
||||
if err != nil {
|
||||
return nil, logerror(err)
|
||||
}
|
||||
|
||||
results = append(results, m)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func nullableString(v sql.NullString) string {
|
||||
if !v.Valid {
|
||||
return "NULL"
|
||||
}
|
||||
|
||||
return v.String
|
||||
}
|
28
internal/drivers/stores/models/converter.go
Normal file
28
internal/drivers/stores/models/converter.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/wwmoraes/anilistarr/internal/entities"
|
||||
)
|
||||
|
||||
func (mapping *Mapping) ToMedia() *entities.Media {
|
||||
return &entities.Media{
|
||||
AnilistID: mapping.AnilistID.String,
|
||||
TvdbID: mapping.TvdbID,
|
||||
}
|
||||
}
|
||||
|
||||
func MappingFromMedia(media *entities.Media) *Mapping {
|
||||
if media == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &Mapping{
|
||||
AnilistID: sql.NullString{
|
||||
String: media.AnilistID,
|
||||
Valid: len(media.AnilistID) > 0,
|
||||
},
|
||||
TvdbID: media.TvdbID,
|
||||
}
|
||||
}
|
241
internal/drivers/stores/models/db.xo.go
Normal file
241
internal/drivers/stores/models/db.xo.go
Normal file
|
@ -0,0 +1,241 @@
|
|||
// Package models contains generated code for schema 'media.db'.
|
||||
package models
|
||||
|
||||
// Code generated by xo. DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// logf is used by generated code to log SQL queries.
|
||||
logf = func(string, ...interface{}) {}
|
||||
// errf is used by generated code to log SQL errors.
|
||||
errf = func(string, ...interface{}) {}
|
||||
)
|
||||
|
||||
// logerror logs the error and returns it.
|
||||
func logerror(err error) error {
|
||||
errf("ERROR: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Logf logs a message using the package logger.
|
||||
func Logf(s string, v ...interface{}) {
|
||||
logf(s, v...)
|
||||
}
|
||||
|
||||
// SetLogger sets the package logger. Valid logger types:
|
||||
//
|
||||
// io.Writer
|
||||
// func(string, ...interface{}) (int, error) // fmt.Printf
|
||||
// func(string, ...interface{}) // log.Printf
|
||||
func SetLogger(logger interface{}) {
|
||||
logf = convLogger(logger)
|
||||
}
|
||||
|
||||
// Errorf logs an error message using the package error logger.
|
||||
func Errorf(s string, v ...interface{}) {
|
||||
errf(s, v...)
|
||||
}
|
||||
|
||||
// SetErrorLogger sets the package error logger. Valid logger types:
|
||||
//
|
||||
// io.Writer
|
||||
// func(string, ...interface{}) (int, error) // fmt.Printf
|
||||
// func(string, ...interface{}) // log.Printf
|
||||
func SetErrorLogger(logger interface{}) {
|
||||
errf = convLogger(logger)
|
||||
}
|
||||
|
||||
// convLogger converts logger to the standard logger interface.
|
||||
func convLogger(logger interface{}) func(string, ...interface{}) {
|
||||
switch z := logger.(type) {
|
||||
case io.Writer:
|
||||
return func(s string, v ...interface{}) {
|
||||
fmt.Fprintf(z, s, v...)
|
||||
}
|
||||
case func(string, ...interface{}) (int, error): // fmt.Printf
|
||||
return func(s string, v ...interface{}) {
|
||||
_, _ = z(s, v...)
|
||||
}
|
||||
case func(string, ...interface{}): // log.Printf
|
||||
return z
|
||||
}
|
||||
panic(fmt.Sprintf("unsupported logger type %T", logger))
|
||||
}
|
||||
|
||||
// DB is the common interface for database operations that can be used with
|
||||
// types from schema 'media.db'.
|
||||
//
|
||||
// This works with both database/sql.DB and database/sql.Tx.
|
||||
type DB interface {
|
||||
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
||||
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
||||
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
||||
}
|
||||
|
||||
// Error is an error.
|
||||
type Error string
|
||||
|
||||
// Error satisfies the error interface.
|
||||
func (err Error) Error() string {
|
||||
return string(err)
|
||||
}
|
||||
|
||||
// Error values.
|
||||
const (
|
||||
// ErrAlreadyExists is the already exists error.
|
||||
ErrAlreadyExists Error = "already exists"
|
||||
// ErrDoesNotExist is the does not exist error.
|
||||
ErrDoesNotExist Error = "does not exist"
|
||||
// ErrMarkedForDeletion is the marked for deletion error.
|
||||
ErrMarkedForDeletion Error = "marked for deletion"
|
||||
)
|
||||
|
||||
// ErrInsertFailed is the insert failed error.
|
||||
type ErrInsertFailed struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error satisfies the error interface.
|
||||
func (err *ErrInsertFailed) Error() string {
|
||||
return fmt.Sprintf("insert failed: %v", err.Err)
|
||||
}
|
||||
|
||||
// Unwrap satisfies the unwrap interface.
|
||||
func (err *ErrInsertFailed) Unwrap() error {
|
||||
return err.Err
|
||||
}
|
||||
|
||||
// ErrUpdateFailed is the update failed error.
|
||||
type ErrUpdateFailed struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error satisfies the error interface.
|
||||
func (err *ErrUpdateFailed) Error() string {
|
||||
return fmt.Sprintf("update failed: %v", err.Err)
|
||||
}
|
||||
|
||||
// Unwrap satisfies the unwrap interface.
|
||||
func (err *ErrUpdateFailed) Unwrap() error {
|
||||
return err.Err
|
||||
}
|
||||
|
||||
// ErrUpsertFailed is the upsert failed error.
|
||||
type ErrUpsertFailed struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error satisfies the error interface.
|
||||
func (err *ErrUpsertFailed) Error() string {
|
||||
return fmt.Sprintf("upsert failed: %v", err.Err)
|
||||
}
|
||||
|
||||
// Unwrap satisfies the unwrap interface.
|
||||
func (err *ErrUpsertFailed) Unwrap() error {
|
||||
return err.Err
|
||||
}
|
||||
|
||||
// ErrInvalidTime is the invalid Time error.
|
||||
type ErrInvalidTime string
|
||||
|
||||
// Error satisfies the error interface.
|
||||
func (err ErrInvalidTime) Error() string {
|
||||
return fmt.Sprintf("invalid Time (%s)", string(err))
|
||||
}
|
||||
|
||||
// Time is a SQLite3 Time that scans for the various timestamps values used by
|
||||
// SQLite3 database drivers to store time.Time values.
|
||||
type Time struct {
|
||||
time time.Time
|
||||
}
|
||||
|
||||
// NewTime creates a time.
|
||||
func NewTime(t time.Time) Time {
|
||||
return Time{time: t}
|
||||
}
|
||||
|
||||
// String satisfies the fmt.Stringer interface.
|
||||
func (t Time) String() string {
|
||||
return t.time.String()
|
||||
}
|
||||
|
||||
// Format formats the time.
|
||||
func (t Time) Format(layout string) string {
|
||||
return t.time.Format(layout)
|
||||
}
|
||||
|
||||
// Time returns a time.Time.
|
||||
func (t Time) Time() time.Time {
|
||||
return t.time
|
||||
}
|
||||
|
||||
// Value satisfies the sql/driver.Valuer interface.
|
||||
func (t Time) Value() (driver.Value, error) {
|
||||
return t.time, nil
|
||||
}
|
||||
|
||||
// Scan satisfies the sql.Scanner interface.
|
||||
func (t *Time) Scan(v interface{}) error {
|
||||
switch x := v.(type) {
|
||||
case time.Time:
|
||||
t.time = x
|
||||
return nil
|
||||
case []byte:
|
||||
return t.Parse(string(x))
|
||||
case string:
|
||||
return t.Parse(x)
|
||||
}
|
||||
return ErrInvalidTime(fmt.Sprintf("%T", v))
|
||||
}
|
||||
|
||||
// Parse attempts to Parse string s to t.
|
||||
func (t *Time) Parse(s string) error {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
for _, f := range TimestampFormats {
|
||||
if z, err := time.Parse(f, s); err == nil {
|
||||
t.time = z
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return ErrInvalidTime(s)
|
||||
}
|
||||
|
||||
// MarshalJSON satisfies the json.Marshaler interface.
|
||||
func (t Time) MarshalJSON() ([]byte, error) {
|
||||
return t.time.MarshalJSON()
|
||||
}
|
||||
|
||||
// UnmarshalJSON satisfies the json.Unmarshaler interface.
|
||||
func (t *Time) UnmarshalJSON(data []byte) error {
|
||||
return t.time.UnmarshalJSON(data)
|
||||
}
|
||||
|
||||
// TimestampFormats are the timestamp formats used by SQLite3 database drivers
|
||||
// to store a time.Time in SQLite3.
|
||||
//
|
||||
// The first format in the slice will be used when saving time values into the
|
||||
// database. When parsing a string from a timestamp or datetime column, the
|
||||
// formats are tried in order.
|
||||
var TimestampFormats = []string{
|
||||
// By default, use timestamps with the timezone they have. When parsed,
|
||||
// they will be returned with the same timezone.
|
||||
"2006-01-02 15:04:05.999999999-07:00",
|
||||
"2006-01-02T15:04:05.999999999-07:00",
|
||||
"2006-01-02 15:04:05.999999999",
|
||||
"2006-01-02T15:04:05.999999999",
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02T15:04:05",
|
||||
"2006-01-02 15:04",
|
||||
"2006-01-02T15:04",
|
||||
"2006-01-02",
|
||||
}
|
165
internal/drivers/stores/models/mapping.xo.go
Normal file
165
internal/drivers/stores/models/mapping.xo.go
Normal file
|
@ -0,0 +1,165 @@
|
|||
package models
|
||||
|
||||
// Code generated by xo. DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// Mapping represents a row from 'mapping'.
|
||||
type Mapping struct {
|
||||
TvdbID string `json:"tvdb_id"` // tvdb_id
|
||||
AnilistID sql.NullString `json:"anilist_id"` // anilist_id
|
||||
// xo fields
|
||||
_exists, _deleted bool
|
||||
}
|
||||
|
||||
// Exists returns true when the Mapping exists in the database.
|
||||
func (m *Mapping) Exists() bool {
|
||||
return m._exists
|
||||
}
|
||||
|
||||
// Deleted returns true when the Mapping has been marked for deletion from
|
||||
// the database.
|
||||
func (m *Mapping) Deleted() bool {
|
||||
return m._deleted
|
||||
}
|
||||
|
||||
// Insert inserts the Mapping to the database.
|
||||
func (m *Mapping) Insert(ctx context.Context, db DB) error {
|
||||
switch {
|
||||
case m._exists: // already exists
|
||||
return logerror(&ErrInsertFailed{ErrAlreadyExists})
|
||||
case m._deleted: // deleted
|
||||
return logerror(&ErrInsertFailed{ErrMarkedForDeletion})
|
||||
}
|
||||
// insert (manual)
|
||||
const sqlstr = `INSERT INTO mapping (` +
|
||||
`tvdb_id, anilist_id` +
|
||||
`) VALUES (` +
|
||||
`$1, $2` +
|
||||
`)`
|
||||
// run
|
||||
logf(sqlstr, m.TvdbID, m.AnilistID)
|
||||
if _, err := db.ExecContext(ctx, sqlstr, m.TvdbID, m.AnilistID); err != nil {
|
||||
return logerror(err)
|
||||
}
|
||||
// set exists
|
||||
m._exists = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update updates a Mapping in the database.
|
||||
func (m *Mapping) Update(ctx context.Context, db DB) error {
|
||||
switch {
|
||||
case !m._exists: // doesn't exist
|
||||
return logerror(&ErrUpdateFailed{ErrDoesNotExist})
|
||||
case m._deleted: // deleted
|
||||
return logerror(&ErrUpdateFailed{ErrMarkedForDeletion})
|
||||
}
|
||||
// update with primary key
|
||||
const sqlstr = `UPDATE mapping SET ` +
|
||||
`anilist_id = $1 ` +
|
||||
`WHERE tvdb_id = $2`
|
||||
// run
|
||||
logf(sqlstr, m.AnilistID, m.TvdbID)
|
||||
if _, err := db.ExecContext(ctx, sqlstr, m.AnilistID, m.TvdbID); err != nil {
|
||||
return logerror(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save saves the Mapping to the database.
|
||||
func (m *Mapping) Save(ctx context.Context, db DB) error {
|
||||
if m.Exists() {
|
||||
return m.Update(ctx, db)
|
||||
}
|
||||
return m.Insert(ctx, db)
|
||||
}
|
||||
|
||||
// Upsert performs an upsert for Mapping.
|
||||
func (m *Mapping) Upsert(ctx context.Context, db DB) error {
|
||||
switch {
|
||||
case m._deleted: // deleted
|
||||
return logerror(&ErrUpsertFailed{ErrMarkedForDeletion})
|
||||
}
|
||||
// upsert
|
||||
const sqlstr = `INSERT INTO mapping (` +
|
||||
`tvdb_id, anilist_id` +
|
||||
`) VALUES (` +
|
||||
`$1, $2` +
|
||||
`)` +
|
||||
` ON CONFLICT (tvdb_id) DO ` +
|
||||
`UPDATE SET ` +
|
||||
`anilist_id = EXCLUDED.anilist_id `
|
||||
// run
|
||||
logf(sqlstr, m.TvdbID, m.AnilistID)
|
||||
if _, err := db.ExecContext(ctx, sqlstr, m.TvdbID, m.AnilistID); err != nil {
|
||||
return logerror(err)
|
||||
}
|
||||
// set exists
|
||||
m._exists = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes the Mapping from the database.
|
||||
func (m *Mapping) Delete(ctx context.Context, db DB) error {
|
||||
switch {
|
||||
case !m._exists: // doesn't exist
|
||||
return nil
|
||||
case m._deleted: // deleted
|
||||
return nil
|
||||
}
|
||||
// delete with single primary key
|
||||
const sqlstr = `DELETE FROM mapping ` +
|
||||
`WHERE tvdb_id = $1`
|
||||
// run
|
||||
logf(sqlstr, m.TvdbID)
|
||||
if _, err := db.ExecContext(ctx, sqlstr, m.TvdbID); err != nil {
|
||||
return logerror(err)
|
||||
}
|
||||
// set deleted
|
||||
m._deleted = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// MappingByTvdbID retrieves a row from 'mapping' as a Mapping.
|
||||
//
|
||||
// Generated from index 'sqlite_autoindex_mapping_1'.
|
||||
func MappingByTvdbID(ctx context.Context, db DB, tvdbID string) (*Mapping, error) {
|
||||
// query
|
||||
const sqlstr = `SELECT ` +
|
||||
`tvdb_id, anilist_id ` +
|
||||
`FROM mapping ` +
|
||||
`WHERE tvdb_id = $1`
|
||||
// run
|
||||
logf(sqlstr, tvdbID)
|
||||
m := Mapping{
|
||||
_exists: true,
|
||||
}
|
||||
if err := db.QueryRowContext(ctx, sqlstr, tvdbID).Scan(&m.TvdbID, &m.AnilistID); err != nil {
|
||||
return nil, logerror(err)
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
// MappingByAnilistID retrieves a row from 'mapping' as a Mapping.
|
||||
//
|
||||
// Generated from index 'sqlite_autoindex_mapping_2'.
|
||||
func MappingByAnilistID(ctx context.Context, db DB, anilistID sql.NullString) (*Mapping, error) {
|
||||
// query
|
||||
const sqlstr = `SELECT ` +
|
||||
`tvdb_id, anilist_id ` +
|
||||
`FROM mapping ` +
|
||||
`WHERE anilist_id = $1`
|
||||
// run
|
||||
logf(sqlstr, anilistID)
|
||||
m := Mapping{
|
||||
_exists: true,
|
||||
}
|
||||
if err := db.QueryRowContext(ctx, sqlstr, anilistID).Scan(&m.TvdbID, &m.AnilistID); err != nil {
|
||||
return nil, logerror(err)
|
||||
}
|
||||
return &m, nil
|
||||
}
|
104
internal/drivers/stores/sql.go
Normal file
104
internal/drivers/stores/sql.go
Normal file
|
@ -0,0 +1,104 @@
|
|||
package stores
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/wwmoraes/anilistarr/internal/drivers/stores/models"
|
||||
"github.com/wwmoraes/anilistarr/internal/entities"
|
||||
"github.com/wwmoraes/anilistarr/internal/telemetry"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
type Sql struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewSQL(driverName, dataSourceName string) (*Sql, error) {
|
||||
db, err := telemetry.OpenSQL(driverName, dataSourceName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open SQL database: %w", err)
|
||||
}
|
||||
|
||||
return &Sql{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Sql) PutMedia(ctx context.Context, media *entities.Media) error {
|
||||
ctx, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
record := models.MappingFromMedia(media)
|
||||
|
||||
return span.Assert(record.Upsert(ctx, s.db))
|
||||
}
|
||||
|
||||
func (s *Sql) PutMediaBulk(ctx context.Context, medias []*entities.Media) error {
|
||||
ctx, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
records := make(models.MappingList, len(medias))
|
||||
for index, media := range medias {
|
||||
records[index] = &models.Mapping{
|
||||
TvdbID: media.TvdbID,
|
||||
AnilistID: sql.NullString{
|
||||
String: media.AnilistID,
|
||||
Valid: len(media.AnilistID) > 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return span.Assert(records.Upsert(ctx, s.db))
|
||||
}
|
||||
|
||||
func (s *Sql) MappingByAnilistID(ctx context.Context, anilistId string) (*entities.Media, error) {
|
||||
ctx, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
record, err := models.MappingByAnilistID(ctx, s.db, sql.NullString{
|
||||
String: anilistId,
|
||||
Valid: len(anilistId) > 0,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, span.Assert(nil)
|
||||
} else if err != nil {
|
||||
return nil, span.Assert(fmt.Errorf("failed to get mapping by anilist ID: %w", err))
|
||||
}
|
||||
|
||||
return record.ToMedia(), span.Assert(nil)
|
||||
}
|
||||
|
||||
func (s *Sql) MappingByAnilistIDBulk(ctx context.Context, anilistIds []string) ([]*entities.Media, error) {
|
||||
ctx, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
ids := make([]sql.NullString, len(anilistIds))
|
||||
for index, id := range anilistIds {
|
||||
ids[index] = sql.NullString{
|
||||
String: id,
|
||||
Valid: len(id) > 0,
|
||||
}
|
||||
}
|
||||
|
||||
records, err := models.MappingByAnilistIDBulk(ctx, s.db, ids)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, span.Assert(nil)
|
||||
} else if err != nil {
|
||||
return nil, span.Assert(fmt.Errorf("failed to get mapping by anilist ID: %w", err))
|
||||
}
|
||||
|
||||
results := make([]*entities.Media, len(records))
|
||||
for index, entry := range records {
|
||||
results[index] = entry.ToMedia()
|
||||
}
|
||||
|
||||
return results, span.Assert(nil)
|
||||
}
|
||||
|
||||
func (s *Sql) Close() error {
|
||||
return s.db.Close()
|
||||
}
|
22
internal/entities/media.go
Normal file
22
internal/entities/media.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package entities
|
||||
|
||||
type Media struct {
|
||||
AnilistID string `json:"anilist_id,omitempty" db:"anilist_id"`
|
||||
TvdbID string `json:"thetvdb_id,omitempty" db:"thetvdb_id"`
|
||||
|
||||
//// useless
|
||||
// Type string `json:"type,omitempty"`
|
||||
|
||||
//// commented out as we don't need these
|
||||
// AnidbID uint `json:"anidb_id,omitempty"`
|
||||
// AnisearchID uint `json:"anisearch_id,omitempty"`
|
||||
// ImdbID string `json:"imdb_id,omitempty"`
|
||||
// KitsuID uint `json:"kitsu_id,omitempty"`
|
||||
// LivechartID uint `json:"livechart_id,omitempty"`
|
||||
// MalID uint `json:"mal_id,omitempty"`
|
||||
// NotifyMoeID string `json:"notify.moe_id,omitempty"`
|
||||
|
||||
//// those are even worse as they mix strings and numbers
|
||||
// AnimePlanetID string `json:"anime-planet_id,omitempty"`
|
||||
// TmdbID uint `json:"themoviedb_id,omitempty"`
|
||||
}
|
7
internal/entities/sonarr.go
Normal file
7
internal/entities/sonarr.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package entities
|
||||
|
||||
type SonarrCustomList []SonarrCustomEntry
|
||||
|
||||
type SonarrCustomEntry struct {
|
||||
TvdbID uint64
|
||||
}
|
11
internal/telemetry/atributable.go
Normal file
11
internal/telemetry/atributable.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package telemetry
|
||||
|
||||
import "go.opentelemetry.io/otel/attribute"
|
||||
|
||||
type Attributable interface {
|
||||
SetAttributes(kv ...attribute.KeyValue)
|
||||
}
|
||||
|
||||
func Int(element Attributable, k string, v int) {
|
||||
element.SetAttributes(attribute.Int(k, v))
|
||||
}
|
11
internal/telemetry/constants.go
Normal file
11
internal/telemetry/constants.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
// Code generated by go generate. DO NOT EDIT.
|
||||
|
||||
package telemetry
|
||||
|
||||
const (
|
||||
ENVIRONMENT = "development"
|
||||
MODULE = "github.com/wwmoraes/anilistarr"
|
||||
VERSION = "0.1.0-rc.1"
|
||||
NAME = "handler"
|
||||
NAMESPACE = "media"
|
||||
)
|
59
internal/telemetry/http.go
Normal file
59
internal/telemetry/http.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package telemetry
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
func NewHandler(handler http.Handler, operation string) http.Handler {
|
||||
return otelhttp.NewHandler(handler, operation)
|
||||
}
|
||||
|
||||
func NewHandlerMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := globalTracer.StartHTTPResponse(r)
|
||||
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
||||
|
||||
next.ServeHTTP(ww, r.WithContext(ctx))
|
||||
span.EndWithStatus(ww.Status())
|
||||
})
|
||||
}
|
||||
|
||||
func NewHandleFunc(fn http.HandlerFunc, operation string) http.Handler {
|
||||
return NewHandler(fn, operation)
|
||||
}
|
||||
|
||||
type responseWriterSnooper struct {
|
||||
w http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (ws *responseWriterSnooper) WriteHeader(statusCode int) {
|
||||
ws.statusCode = statusCode
|
||||
ws.w.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func (ws *responseWriterSnooper) Header() http.Header {
|
||||
return ws.w.Header()
|
||||
}
|
||||
func (ws *responseWriterSnooper) Write(data []byte) (int, error) {
|
||||
return ws.w.Write(data)
|
||||
}
|
||||
|
||||
func HandlerFunc(fn http.HandlerFunc, startOptions []trace.SpanStartOption, endOptions []trace.SpanEndOption) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := globalTracer.StartHTTPResponse(r, startOptions...)
|
||||
|
||||
res := responseWriterSnooper{
|
||||
w: w,
|
||||
statusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
fn(&res, r.WithContext(ctx))
|
||||
|
||||
span.EndWithStatus(res.statusCode, endOptions...)
|
||||
}
|
||||
}
|
41
internal/telemetry/httpspan.go
Normal file
41
internal/telemetry/httpspan.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package telemetry
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
type HTTPSpan interface {
|
||||
Span
|
||||
|
||||
HTTPStatus(status int)
|
||||
EndWithStatus(status int, options ...trace.SpanEndOption)
|
||||
}
|
||||
|
||||
type httpSpan struct {
|
||||
span
|
||||
}
|
||||
|
||||
func (s *httpSpan) HTTPStatus(status int) {
|
||||
code := codes.Ok
|
||||
if status >= 400 {
|
||||
code = codes.Error
|
||||
}
|
||||
|
||||
s.SetAttributes(semconv.HTTPStatusCode(status))
|
||||
s.SetStatus(code, http.StatusText(status))
|
||||
}
|
||||
|
||||
func (s *httpSpan) EndWithStatus(status int, options ...trace.SpanEndOption) {
|
||||
code := codes.Ok
|
||||
if status >= 400 {
|
||||
code = codes.Error
|
||||
}
|
||||
|
||||
s.SetAttributes(semconv.HTTPStatusCode(status))
|
||||
s.SetStatus(code, http.StatusText(status))
|
||||
s.End(options...)
|
||||
}
|
180
internal/telemetry/instrument.go
Normal file
180
internal/telemetry/instrument.go
Normal file
|
@ -0,0 +1,180 @@
|
|||
package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/MrAlias/otlpr"
|
||||
"github.com/go-logr/logr"
|
||||
otelruntime "go.opentelemetry.io/contrib/instrumentation/runtime"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
var (
|
||||
otlpConnHandler sync.Once
|
||||
otlpConn *grpc.ClientConn
|
||||
otlpConnErr error
|
||||
|
||||
otlpResource *resource.Resource
|
||||
|
||||
globalTracer Tracer
|
||||
globalMeter Meter
|
||||
globalLogger Logger
|
||||
)
|
||||
|
||||
func init() {
|
||||
otel.SetTextMapPropagator(
|
||||
propagation.NewCompositeTextMapPropagator(
|
||||
propagation.TraceContext{},
|
||||
propagation.Baggage{},
|
||||
),
|
||||
)
|
||||
|
||||
var err error
|
||||
otlpResource, err = resource.Merge(resource.Empty(), resource.NewWithAttributes(
|
||||
semconv.SchemaURL,
|
||||
semconv.ServiceNamespace(NAMESPACE),
|
||||
semconv.ServiceName(NAME),
|
||||
semconv.ServiceVersion(VERSION),
|
||||
semconv.CodeNamespace(MODULE),
|
||||
semconv.DeploymentEnvironment(ENVIRONMENT),
|
||||
))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to create OTLP resource: %s", err.Error())
|
||||
}
|
||||
|
||||
globalTracer = newTracer()
|
||||
globalMeter = newMeter()
|
||||
globalLogger = logr.New(NewStdLogSink())
|
||||
}
|
||||
|
||||
func getOTLPConnGRPC(ctx context.Context, otlpEndpoint string) (*grpc.ClientConn, error) {
|
||||
otlpConnHandler.Do(func() {
|
||||
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
otlpConn, otlpConnErr = grpc.DialContext(ctx, otlpEndpoint,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithBlock(),
|
||||
)
|
||||
if otlpConnErr != nil {
|
||||
otlpConnErr = fmt.Errorf("failed to connect to the OTLP endpoint: %w", otlpConnErr)
|
||||
}
|
||||
})
|
||||
|
||||
return otlpConn, otlpConnErr
|
||||
}
|
||||
|
||||
func providerShutdown(shutdown func(context.Context) error) func(context.Context) {
|
||||
return func(ctx context.Context) {
|
||||
if err := shutdown(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func InstrumentTracing(ctx context.Context, otlpEndpoint string) (func(context.Context), error) {
|
||||
conn, err := getOTLPConnGRPC(ctx, otlpEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
traceExporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create an OTLP exporter: %w", err)
|
||||
}
|
||||
|
||||
bsp := sdktrace.NewBatchSpanProcessor(traceExporter)
|
||||
traceProvider := sdktrace.NewTracerProvider(
|
||||
|
||||
sdktrace.WithSampler(sdktrace.AlwaysSample()),
|
||||
sdktrace.WithResource(otlpResource),
|
||||
sdktrace.WithSpanProcessor(bsp),
|
||||
// flow.WithSpanProcessor(bsp),
|
||||
)
|
||||
|
||||
otel.SetTracerProvider(traceProvider)
|
||||
|
||||
return providerShutdown(traceProvider.Shutdown), nil
|
||||
}
|
||||
|
||||
func InstrumentMetrics(ctx context.Context, otlpEndpoint string) (func(context.Context), error) {
|
||||
conn, err := getOTLPConnGRPC(ctx, otlpEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
meterExporter, err := otlpmetricgrpc.New(ctx, otlpmetricgrpc.WithGRPCConn(conn))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create an OTLP exporter: %w", err)
|
||||
}
|
||||
|
||||
meterProvider := sdkmetric.NewMeterProvider(
|
||||
sdkmetric.WithResource(otlpResource),
|
||||
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(meterExporter)),
|
||||
)
|
||||
|
||||
err = otelruntime.Start(
|
||||
otelruntime.WithMeterProvider(meterProvider),
|
||||
otelruntime.WithMinimumReadMemStatsInterval(time.Second),
|
||||
)
|
||||
if err != nil {
|
||||
providerShutdown(meterProvider.Shutdown)(ctx)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
otel.SetMeterProvider(meterProvider)
|
||||
|
||||
return providerShutdown(meterProvider.Shutdown), nil
|
||||
}
|
||||
|
||||
func InstrumentLogging(ctx context.Context, otlpEndpoint string) error {
|
||||
conn, err := getOTLPConnGRPC(ctx, otlpEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger := otlpr.WithResource(otlpr.New(conn), otlpResource)
|
||||
otlpSink := logger.GetSink()
|
||||
|
||||
globalLogger = logger.WithSink(TeeSink(globalLogger.GetSink(), otlpSink))
|
||||
|
||||
otel.SetLogger(globalLogger)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func InstrumentAll(ctx context.Context, otlpEndpoint string) (func(context.Context), error) {
|
||||
tracingShutdown, err := InstrumentTracing(ctx, otlpEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metricsShutdown, err := InstrumentMetrics(ctx, otlpEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = InstrumentLogging(ctx, otlpEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(ctx context.Context) {
|
||||
tracingShutdown(ctx)
|
||||
metricsShutdown(ctx)
|
||||
}, nil
|
||||
}
|
64
internal/telemetry/logger.go
Normal file
64
internal/telemetry/logger.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
)
|
||||
|
||||
const (
|
||||
keyValueSeparator = ": "
|
||||
entrySeparator = " | "
|
||||
)
|
||||
|
||||
type Logger = logr.Logger
|
||||
|
||||
func DefaultLogger() logr.Logger {
|
||||
return globalLogger
|
||||
}
|
||||
|
||||
func ContextWithLogger(ctx context.Context) context.Context {
|
||||
return logr.NewContext(ctx, globalLogger)
|
||||
}
|
||||
|
||||
func LoggerFromContext(ctx context.Context) logr.Logger {
|
||||
return logr.FromContextOrDiscard(ctx)
|
||||
}
|
||||
|
||||
func kv2Map(keysAndValues ...interface{}) map[interface{}]interface{} {
|
||||
values := make(map[interface{}]interface{}, len(keysAndValues)/2)
|
||||
|
||||
for i := 0; i+1 < len(keysAndValues); i = i + 2 {
|
||||
values[keysAndValues[i]] = keysAndValues[i+1]
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
func mergeMaps(maps ...map[interface{}]interface{}) map[interface{}]interface{} {
|
||||
values := make(map[interface{}]interface{})
|
||||
|
||||
for _, entry := range maps {
|
||||
for k, v := range entry {
|
||||
values[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
func mapString(m map[interface{}]interface{}) string {
|
||||
entries := make([]string, 0, len(m))
|
||||
|
||||
for k, v := range m {
|
||||
if k == nil || v == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
entries = append(entries, fmt.Sprintf("%v%s%v", k, keyValueSeparator, v))
|
||||
}
|
||||
|
||||
return strings.Join(entries, entrySeparator)
|
||||
}
|
17
internal/telemetry/meter.go
Normal file
17
internal/telemetry/meter.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package telemetry
|
||||
|
||||
import (
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
)
|
||||
|
||||
type Meter = metric.Meter
|
||||
type MeterOption = metric.MeterOption
|
||||
|
||||
func newMeter(opts ...MeterOption) Meter {
|
||||
return otel.Meter(NAME, opts...)
|
||||
}
|
||||
|
||||
func DefaultMeter() Meter {
|
||||
return globalMeter
|
||||
}
|
55
internal/telemetry/span.go
Normal file
55
internal/telemetry/span.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package telemetry
|
||||
|
||||
import (
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
type Span interface {
|
||||
trace.Span
|
||||
|
||||
EndWith(err error, options ...trace.SpanEndOption)
|
||||
Assert(error) error
|
||||
Int(k string, v int)
|
||||
}
|
||||
|
||||
type span struct {
|
||||
trace.Span
|
||||
}
|
||||
|
||||
func (s *span) Assert(err error) error {
|
||||
if err == nil {
|
||||
s.SetStatus(codes.Ok, "")
|
||||
} else {
|
||||
s.SetStatus(codes.Error, err.Error())
|
||||
s.RecordError(err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *span) EndWith(err error, options ...trace.SpanEndOption) {
|
||||
if err == nil {
|
||||
s.SetStatus(codes.Ok, "")
|
||||
} else {
|
||||
s.SetStatus(codes.Error, err.Error())
|
||||
s.RecordError(err)
|
||||
}
|
||||
|
||||
s.End(options...)
|
||||
}
|
||||
|
||||
func (s *span) Int(k string, v int) {
|
||||
s.SetAttributes(attribute.Int(k, v))
|
||||
}
|
||||
|
||||
func WithInt(k string, v int) trace.SpanStartEventOption {
|
||||
return trace.WithAttributes(
|
||||
attribute.Int(k, v),
|
||||
)
|
||||
}
|
||||
|
||||
func WithSpanKindClient() trace.SpanStartOption {
|
||||
return trace.WithSpanKind(trace.SpanKindClient)
|
||||
}
|
56
internal/telemetry/stdlogsink.go
Normal file
56
internal/telemetry/stdlogsink.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package telemetry
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
)
|
||||
|
||||
type stdLogSink struct {
|
||||
stdout *log.Logger
|
||||
stderr *log.Logger
|
||||
values map[interface{}]interface{}
|
||||
}
|
||||
|
||||
func NewStdLogSink() *stdLogSink {
|
||||
return &stdLogSink{
|
||||
stdout: log.New(os.Stdout, "", 0),
|
||||
stderr: log.New(os.Stderr, "", 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (sink *stdLogSink) Enabled(level int) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (sink *stdLogSink) Error(err error, msg string, keysAndValues ...interface{}) {
|
||||
values := mergeMaps(sink.values, kv2Map(keysAndValues...))
|
||||
|
||||
sink.stderr.Printf("%s: %s [%s]", msg, err.Error(), mapString(values))
|
||||
}
|
||||
|
||||
func (sink *stdLogSink) Info(level int, msg string, keysAndValues ...interface{}) {
|
||||
values := mergeMaps(sink.values, kv2Map(keysAndValues...))
|
||||
|
||||
sink.stdout.Printf("%s [%s] %s", time.Now().Format(time.Stamp), mapString(values), msg)
|
||||
}
|
||||
|
||||
func (sink *stdLogSink) Init(info logr.RuntimeInfo) {}
|
||||
|
||||
func (sink *stdLogSink) WithValues(keysAndValues ...interface{}) logr.LogSink {
|
||||
return &stdLogSink{
|
||||
stdout: log.New(sink.stdout.Writer(), sink.stdout.Prefix(), sink.stdout.Flags()),
|
||||
stderr: log.New(sink.stderr.Writer(), sink.stderr.Prefix(), sink.stderr.Flags()),
|
||||
values: mergeMaps(sink.values, kv2Map(keysAndValues...)),
|
||||
}
|
||||
}
|
||||
|
||||
func (sink *stdLogSink) WithName(name string) logr.LogSink {
|
||||
return &stdLogSink{
|
||||
stdout: log.New(sink.stdout.Writer(), sink.stdout.Prefix()+name, sink.stderr.Flags()),
|
||||
stderr: log.New(sink.stderr.Writer(), sink.stderr.Prefix()+name, sink.stderr.Flags()),
|
||||
values: sink.values,
|
||||
}
|
||||
}
|
57
internal/telemetry/teesink.go
Normal file
57
internal/telemetry/teesink.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package telemetry
|
||||
|
||||
import "github.com/go-logr/logr"
|
||||
|
||||
type teeSink []logr.LogSink
|
||||
|
||||
func TeeSink(sinks ...logr.LogSink) logr.LogSink {
|
||||
return teeSink(sinks)
|
||||
}
|
||||
|
||||
func (sinks teeSink) Enabled(level int) bool {
|
||||
for _, sink := range sinks {
|
||||
if !sink.Enabled(level) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (sinks teeSink) Error(err error, msg string, keysAndValues ...interface{}) {
|
||||
for _, sink := range sinks {
|
||||
sink.Error(err, msg, keysAndValues...)
|
||||
}
|
||||
}
|
||||
|
||||
func (sinks teeSink) Info(level int, msg string, keysAndValues ...interface{}) {
|
||||
for _, sink := range sinks {
|
||||
sink.Info(level, msg, keysAndValues...)
|
||||
}
|
||||
}
|
||||
|
||||
func (sinks teeSink) Init(info logr.RuntimeInfo) {
|
||||
for _, sink := range sinks {
|
||||
sink.Init(info)
|
||||
}
|
||||
}
|
||||
|
||||
func (sinks teeSink) WithValues(keysAndValues ...interface{}) logr.LogSink {
|
||||
newSinks := make(teeSink, len(sinks))
|
||||
|
||||
for index, sink := range sinks {
|
||||
newSinks[index] = sink.WithValues(keysAndValues...)
|
||||
}
|
||||
|
||||
return newSinks
|
||||
}
|
||||
|
||||
func (sinks teeSink) WithName(name string) logr.LogSink {
|
||||
newSinks := make(teeSink, len(sinks))
|
||||
|
||||
for index, sink := range sinks {
|
||||
newSinks[index] = sink.WithName(name)
|
||||
}
|
||||
|
||||
return newSinks
|
||||
}
|
55
internal/telemetry/telemetry.go
Normal file
55
internal/telemetry/telemetry.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/XSAM/otelsql"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
func StartFunction(ctx context.Context, opts ...trace.SpanStartOption) (context.Context, Span) {
|
||||
name, opt := functionInfo(2)
|
||||
opts = append(opts, opt)
|
||||
|
||||
return globalTracer.Start(ctx, name, opts...)
|
||||
}
|
||||
|
||||
func Start(ctx context.Context, spanName string, opts ...SpanStartOption) (context.Context, Span) {
|
||||
return globalTracer.Start(ctx, spanName, opts...)
|
||||
}
|
||||
|
||||
func SpanFromContext(ctx context.Context) trace.Span {
|
||||
return trace.SpanFromContext(ctx)
|
||||
}
|
||||
|
||||
func WantedRequestHeaders(h http.Header, keys ...string) http.Header {
|
||||
target := http.Header{}
|
||||
|
||||
for _, key := range keys {
|
||||
target[key] = h.Values(key)
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
||||
|
||||
func OpenSQL(driverName, dataSourceName string) (*sql.DB, error) {
|
||||
attributes := otelsql.WithAttributes(
|
||||
attribute.String(string(semconv.DBSystemKey), driverName),
|
||||
)
|
||||
|
||||
db, err := otelsql.Open(driverName, dataSourceName, attributes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = otelsql.RegisterDBStatsMetrics(db, attributes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
92
internal/telemetry/tracer.go
Normal file
92
internal/telemetry/tracer.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"runtime"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var (
|
||||
fnNameRE = regexp.MustCompile(`.*?(?:\(\*)?([^\./\(\)\[\]]+)(?:[\[\]\.\)]*)?\.([^\./\(\)]+)$`)
|
||||
)
|
||||
|
||||
type TracerOption = trace.TracerOption
|
||||
type SpanStartOption = trace.SpanStartOption
|
||||
|
||||
type Tracer interface {
|
||||
StartFunction(ctx context.Context, opts ...SpanStartOption) (context.Context, Span)
|
||||
StartHTTPResponse(req *http.Request, opts ...SpanStartOption) (context.Context, HTTPSpan)
|
||||
|
||||
// custom span
|
||||
// from trace.Tracer
|
||||
Start(ctx context.Context, spanName string, opts ...SpanStartOption) (context.Context, Span)
|
||||
}
|
||||
|
||||
type tracer struct {
|
||||
upstream trace.Tracer
|
||||
}
|
||||
|
||||
func newTracer(opts ...TracerOption) Tracer {
|
||||
return &tracer{
|
||||
upstream: otel.Tracer(NAME, opts...),
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultTracer() Tracer {
|
||||
return globalTracer
|
||||
}
|
||||
|
||||
func (t *tracer) Start(ctx context.Context, spanName string, opts ...SpanStartOption) (context.Context, Span) {
|
||||
ctx, upstreamSpan := t.upstream.Start(ctx, spanName, opts...)
|
||||
return ctx, &span{upstreamSpan}
|
||||
}
|
||||
|
||||
func (t *tracer) StartFunction(ctx context.Context, opts ...SpanStartOption) (context.Context, Span) {
|
||||
name, opt := functionInfo(1)
|
||||
opts = append(opts, opt)
|
||||
|
||||
return t.Start(ctx, name, opts...)
|
||||
}
|
||||
|
||||
func (t *tracer) StartHTTPResponse(req *http.Request, opts ...SpanStartOption) (context.Context, HTTPSpan) {
|
||||
opts = append(opts, trace.WithAttributes(
|
||||
semconv.HTTPMethod(req.Method),
|
||||
semconv.HTTPURL(req.URL.String()),
|
||||
semconv.HTTPRoute(req.URL.Path),
|
||||
semconv.HTTPScheme(req.URL.Scheme),
|
||||
semconv.HTTPClientIP(req.RemoteAddr),
|
||||
semconv.HTTPRequestContentLength(int(req.ContentLength)),
|
||||
))
|
||||
|
||||
upstreamCtx, upstreamSpan := t.upstream.Start(req.Context(), fmt.Sprintf("%s %s", req.Method, req.URL.Path), opts...)
|
||||
|
||||
return upstreamCtx, &httpSpan{span{upstreamSpan}}
|
||||
}
|
||||
|
||||
func functionInfo(skip int) (string, trace.SpanStartOption) {
|
||||
name, fullName := "", ""
|
||||
pc, file, line, ok := runtime.Caller(skip)
|
||||
if ok {
|
||||
details := runtime.FuncForPC(pc)
|
||||
if details != nil {
|
||||
fullName = details.Name()
|
||||
name = fnNameRE.ReplaceAllString(details.Name(), "$1.$2")
|
||||
file, line = details.FileLine(pc)
|
||||
}
|
||||
} else {
|
||||
name = file
|
||||
fullName = file
|
||||
}
|
||||
|
||||
return name, trace.WithAttributes(
|
||||
semconv.CodeFunction(fullName),
|
||||
semconv.CodeFilepath(file),
|
||||
semconv.CodeLineNumber(line),
|
||||
)
|
||||
}
|
14
internal/usecases/mapper.go
Normal file
14
internal/usecases/mapper.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package usecases
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Mapper interface {
|
||||
io.Closer
|
||||
|
||||
MapIDs(context.Context, []string) ([]string, error)
|
||||
MapID(context.Context, string) (string, error)
|
||||
Refresh(context.Context) error
|
||||
}
|
83
internal/usecases/mediabridge.go
Normal file
83
internal/usecases/mediabridge.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package usecases
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/wwmoraes/anilistarr/internal/entities"
|
||||
"github.com/wwmoraes/anilistarr/internal/telemetry"
|
||||
)
|
||||
|
||||
type MediaBridge struct {
|
||||
Tracker Tracker
|
||||
Mapper Mapper
|
||||
}
|
||||
|
||||
func (linker *MediaBridge) GenerateCustomList(ctx context.Context, name string) (entities.SonarrCustomList, error) {
|
||||
log := telemetry.LoggerFromContext(ctx).WithValues("username", name)
|
||||
ctx, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
log.Info("retrieving user ID")
|
||||
userId, err := linker.GetUserID(ctx, name)
|
||||
if err != nil {
|
||||
return nil, span.Assert(fmt.Errorf("failed to get user ID: %w", err))
|
||||
}
|
||||
|
||||
log.Info("retrieving media list IDs", "userID", userId)
|
||||
sourceIds, err := linker.Tracker.GetMediaListIDs(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, span.Assert(fmt.Errorf("failed to get media list IDs: %w", err))
|
||||
}
|
||||
|
||||
targetIds, err := linker.Mapper.MapIDs(ctx, sourceIds)
|
||||
if err != nil {
|
||||
return nil, span.Assert(fmt.Errorf("failed to get mapped IDs: %w", err))
|
||||
}
|
||||
|
||||
customList := make(entities.SonarrCustomList, 0, len(targetIds))
|
||||
for index, entry := range targetIds {
|
||||
if entry == "" {
|
||||
log.Info("no TVDB ID registered for source ID", "sourceID", sourceIds[index])
|
||||
continue
|
||||
}
|
||||
|
||||
tvdbID, err := strconv.ParseUint(entry, 10, 0)
|
||||
if err != nil {
|
||||
return nil, span.Assert(fmt.Errorf("failed to parse TVDB ID: %w", err))
|
||||
}
|
||||
|
||||
customList = append(customList, entities.SonarrCustomEntry{
|
||||
TvdbID: tvdbID,
|
||||
})
|
||||
}
|
||||
|
||||
return customList, span.Assert(nil)
|
||||
}
|
||||
|
||||
func (linker *MediaBridge) GetUserID(ctx context.Context, name string) (string, error) {
|
||||
ctx, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
res, err := linker.Tracker.GetUserID(ctx, name)
|
||||
return res, span.Assert(err)
|
||||
}
|
||||
|
||||
func (linker *MediaBridge) Close() error {
|
||||
errT := linker.Tracker.Close()
|
||||
errR := linker.Mapper.Close()
|
||||
|
||||
if errT != nil || errR != nil {
|
||||
return fmt.Errorf("failed to close mapper dependencies: %v", []error{errT, errR})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (linker *MediaBridge) Refresh(ctx context.Context) error {
|
||||
ctx, span := telemetry.StartFunction(ctx)
|
||||
defer span.End()
|
||||
|
||||
return span.Assert(linker.Mapper.Refresh(ctx))
|
||||
}
|
13
internal/usecases/tracker.go
Normal file
13
internal/usecases/tracker.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package usecases
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Tracker interface {
|
||||
io.Closer
|
||||
|
||||
GetUserID(ctx context.Context, name string) (string, error)
|
||||
GetMediaListIDs(ctx context.Context, userId string) ([]string, error)
|
||||
}
|
8
media.db.sql
Normal file
8
media.db.sql
Normal file
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE mapping (
|
||||
tvdb_id TEXT UNIQUE
|
||||
PRIMARY KEY
|
||||
NOT NULL,
|
||||
anilist_id TEXT UNIQUE
|
||||
)
|
||||
WITHOUT ROWID,
|
||||
STRICT;
|
17
sonar-project.properties
Normal file
17
sonar-project.properties
Normal file
|
@ -0,0 +1,17 @@
|
|||
sonar.host.url=https://sonarcloud.io
|
||||
sonar.organization=wwmoraes
|
||||
sonar.projectKey=wwmoraes_anilistarr
|
||||
|
||||
sonar.sourceEncoding=UTF-8
|
||||
|
||||
sonar.sources=.
|
||||
sonar.exclusions=**/*_test.go,**/vendor/**
|
||||
sonar.tests=.
|
||||
sonar.language=go
|
||||
|
||||
sonar.test.inclusions=**/*_test.go
|
||||
sonar.test.exclusions=**/vendor/**
|
||||
|
||||
sonar.go.golangci-lint.reportPaths=golangci-lint-report.xml
|
||||
sonar.go.tests.reportPaths=test-report.json
|
||||
sonar.go.coverage.reportPaths=coverage.out
|
13
workspace.dsl
Normal file
13
workspace.dsl
Normal file
|
@ -0,0 +1,13 @@
|
|||
workspace anilistarr "list provider for *arr applications" {
|
||||
|
||||
model {
|
||||
anilistarr = softwareSystem anilistarr "converts anime sources" {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
views {
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue