Compare commits

..

No commits in common. "main" and "audiolion-audiolion/support-foils" have entirely different histories.

45 changed files with 1308 additions and 2385 deletions

3
.gitignore vendored
View File

@ -1,6 +1,7 @@
.DS_Store .DS_Store
serra
!src/serra
_db/* _db/*
/serra
!_db/.placeholder !_db/.placeholder
_backup/* _backup/*
!_backup/.placeholder !_backup/.placeholder

View File

@ -1,51 +0,0 @@
version: 2
before:
hooks:
- go mod tidy
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
- openbsd
ldflags:
- -X github.com/noqqe/serra/pkg/serra.Version={{.Tag}}
flags:
- -v
main: ./cmd/serra
archives:
- id: serra
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
brews:
- name: serra
goarm: 6
repository:
owner: noqqe
name: homebrew-tap
download_strategy: CurlDownloadStrategy
directory: Formula
commit_author:
name: goreleaserbot
email: goreleaser@carlosbecker.com
homepage: "https://github.com/noqqe/serra"
description: "serra - Personal Magic: The Gathering Collection Tracker "
license: "MIT"
test: |
system "#{bin}/serra --version"
install: |
bin.install "serra"

97
.goreleaser.yml Normal file
View File

@ -0,0 +1,97 @@
# This is an example .goreleaser.yml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
# you may remove this if you don't need go generate
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
- openbsd
ldflags:
- -X github.com/noqqe/serra/src/serra.Version={{.Tag}}
flags:
- -v
archives:
- id: serra
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
brews:
-
# Name template of the recipe
# Default to project name
name: serra
# GOARM to specify which 32-bit arm version to use if there are multiple versions
# from the build section. Brew formulas support atm only one 32-bit version.
# Default is 6 for all artifacts or each id if there a multiple versions.
goarm: 6
# NOTE: make sure the url_template, the token and given repo (github or gitlab) owner and name are from the
# same kind. We will probably unify this in the next major version like it is done with scoop.
# GitHub/GitLab repository to push the formula to
# Gitea is not supported yet, but the support coming
tap:
owner: noqqe
name: homebrew-tap
# Optionally a token can be provided, if it differs from the token provided to GoReleaser
# Allows you to set a custom download strategy. Note that you'll need
# to implement the strategy and add it to your tap repository.
# Example: https://docs.brew.sh/Formula-Cookbook#specifying-the-download-strategy-explicitly
# Default is empty.
download_strategy: CurlDownloadStrategy
# Git author used to commit to the repository.
# Defaults are shown.
commit_author:
name: goreleaserbot
email: goreleaser@carlosbecker.com
# Folder inside the repository to put the formula.
# Default is the root folder.
folder: Formula
# Your app's homepage.
# Default is empty.
homepage: "https://github.com/noqqe/serra"
# Your app's description.
# Default is empty.
description: "serra - Personal Magic: The Gathering Collection Tracker "
# SPDX identifier of your app's license.
# Default is empty.
license: "MIT"
# So you can `brew test` your formula.
# Default is empty.
test: |
system "#{bin}/serra --version"
# Custom install script for brew.
# Default is 'bin.install "program"'.
install: |
bin.install "serra"

View File

@ -1 +1 @@
golang 1.21.3 golang 1.20

View File

@ -1,24 +1,23 @@
FROM golang:1.25-alpine AS build FROM golang:alpine AS builder
RUN apk update && apk add --no-cache git ca-certificates curl RUN apk update && apk add --no-cache git
WORKDIR /go/src/app WORKDIR /go/src/app
COPY pkg /go/src/app/pkg COPY src /go/src/app/src
COPY cmd /go/src/app/cmd
COPY templates /go/src/app/templates COPY templates /go/src/app/templates
COPY go.mod /go/src/app/go.mod COPY go.mod /go/src/app/go.mod
COPY go.sum /go/src/app/go.sum COPY go.sum /go/src/app/go.sum
COPY .git /go/src/app/.git COPY .git /go/src/app/.git
COPY serra.go /go/src/app/serra.go
# build # build
RUN go build -ldflags "-X github.com/noqqe/serra/pkg/serra.Version=`git describe --tags`" -v cmd/serra/serra.go RUN go get -v ./...
RUN go build -ldflags "-X github.com/noqqe/serra/src/serra.Version=`git describe --tags`" -v serra.go
# copy # copy
FROM scratch FROM scratch
WORKDIR /go/src/app WORKDIR /go/src/app
COPY --from=build /go/src/app/serra /go/src/app/serra COPY --from=builder /go/src/app/serra /go/src/app/serra
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=build /tmp /tmp
COPY templates /go/src/app/templates COPY templates /go/src/app/templates
# run # run

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 Florian Baumann
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.

View File

@ -5,17 +5,17 @@ version: '3'
tasks: tasks:
build: build:
cmds: cmds:
- go build -ldflags "-X github.com/noqqe/serra/pkg/serra.Version=`git describe --tags`" -v cmd/serra/serra.go - go build -ldflags "-X github.com/noqqe/serra/src/serra.Version=`git describe --tags`" -v serra.go
sources: sources:
- "pkg/serra/**/*.go" - "src/serra/**/*.go"
- "cmd/serra/serra.go" - "serra.go"
generates: generates:
- "./serra" - "./serra"
release: release:
interactive: true interactive: true
cmds: cmds:
- git tag | sort -t. -k 1,1n -k 2,2n -k 3,3n | tail -5 - git tag | tail -5
- read -p "Version v1.1.1 " version ; git tag $version - read -p "Version v1.1.1 " version ; git tag $version
- git push --tags - git push --tags
- goreleaser release --clean - goreleaser release --clean

View File

@ -2,7 +2,7 @@ version: '3.6'
services: services:
mongo: mongo:
image: mongo:6 image: mongo
restart: always restart: always
ports: ports:
- 27017:27017 - 27017:27017

72
go.mod
View File

@ -1,67 +1,23 @@
module github.com/noqqe/serra module github.com/noqqe/serra
go 1.25.0 go 1.14
toolchain go1.25.4
require ( require (
github.com/charmbracelet/log v0.4.0
github.com/chzyer/readline v1.5.1 github.com/chzyer/readline v1.5.1
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.9.0
github.com/mitchellh/mapstructure v1.5.0
github.com/schollz/progressbar/v3 v3.16.1
github.com/spf13/cobra v1.8.1
go.mongodb.org/mongo-driver v1.17.1
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/bytedance/sonic v1.12.3 // indirect
github.com/bytedance/sonic/loader v0.2.1 // indirect
github.com/charmbracelet/lipgloss v0.13.0 // indirect
github.com/charmbracelet/x/ansi v0.3.2 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dolmen-go/kittyimg v0.0.0-20250610224728-874967bd8ea4 // indirect
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.22.1 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.15 // indirect
github.com/klauspost/compress v1.17.11 // indirect github.com/mitchellh/mapstructure v1.5.0
github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/montanaflynn/stats v0.7.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/schollz/progressbar/v3 v3.13.0
github.com/mattn/go-isatty v0.0.20 // indirect github.com/spf13/cobra v1.6.1
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.mongodb.org/mongo-driver v1.11.3
golang.org/x/arch v0.11.0 // indirect golang.org/x/crypto v0.6.0 // indirect
golang.org/x/crypto v0.28.0 // indirect golang.org/x/sync v0.1.0 // indirect
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/term v0.29.0 // indirect
golang.org/x/text v0.19.0 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

207
go.sum
View File

@ -1,180 +1,195 @@
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU= github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY=
github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dolmen-go/kittyimg v0.0.0-20250610224728-874967bd8ea4 h1:KGRb+vxMx5pGsfDjDSW2Th+b2OEflb0yC3s0daCmiYU=
github.com/dolmen-go/kittyimg v0.0.0-20250610224728-874967bd8ea4/go.mod h1:2vk7ATPVcI7uW4Sh6PrSQvtO+Czmq8509xcg/y8Osd0=
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/schollz/progressbar/v3 v3.16.1 h1:RnF1neWZFzLCoGx8yp1yF7SDl4AzNDI5y4I0aUJRrZQ= github.com/schollz/progressbar/v3 v3.13.0 h1:9TeeWRcjW2qd05I8Kf9knPkW4vLM/hYoa6z9ABvxje8=
github.com/schollz/progressbar/v3 v3.16.1/go.mod h1:I2ILR76gz5VXqYMIY/LdLecvMHDPVcQm3W/MSKi1TME= github.com/schollz/progressbar/v3 v3.13.0/go.mod h1:ZBYnSuLAX2LU8P8UiKN/KgF2DY58AJC8yfVYLPC8Ly4=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= go.mongodb.org/mongo-driver v1.11.3 h1:Ql6K6qYHEzB6xvu4+AU0BoRoqf9vFPcc4o7MUIdPW8Y=
go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= go.mongodb.org/mongo-driver v1.11.3/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

@ -1,190 +0,0 @@
package serra
import (
"fmt"
"strconv"
"strings"
"github.com/chzyer/readline"
"github.com/spf13/cobra"
"go.mongodb.org/mongo-driver/bson"
)
func init() {
addCmd.Flags().Int64VarP(&count, "count", "c", 1, "Amount of cards to add")
addCmd.Flags().BoolVarP(&unique, "unique", "u", false, "Only add card if not existent yet")
addCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Spin up interactive terminal")
addCmd.Flags().StringVarP(&set, "set", "s", "", "Filter by set code (usg/mmq/vow)")
addCmd.Flags().BoolVarP(&foil, "foil", "f", false, "Add foil variant of card")
rootCmd.AddCommand(addCmd)
}
var addCmd = &cobra.Command{
Aliases: []string{"a"},
Use: "add",
Short: "Add a card to your collection",
Long: "Adds a card from scryfall to your collection. Amount can be modified using flags",
SilenceErrors: true,
RunE: func(cmd *cobra.Command, cards []string) error {
if interactive {
addCardsInteractive(unique, set)
} else {
addCards(cards, unique, count)
}
return nil
},
}
func addCardsInteractive(unique bool, set string) {
l := Logger()
if len(set) == 0 {
l.Fatal("Option --set <set> must be given in interactive mode")
}
rl, err := readline.New(fmt.Sprintf("%s> ", set))
if err != nil {
panic(err)
}
defer rl.Close()
for {
line, err := rl.Readline()
if err != nil { // io.EOF
break
}
// default is no foil
foil = false
// default is count 1
count = 1
// construct card input for addCards
card := []string{}
// Detect if input contains a dash, if it does it means the user wants to add a range of cards
if strings.Contains(line, "-") {
// Split input into two parts
parts := strings.Split(line, "-")
// Check if both parts are numbers
if _, err := strconv.Atoi(parts[0]); err == nil {
if _, err = strconv.Atoi(parts[1]); err == nil {
// Loop over range and add each card to card slice
start, _ := strconv.Atoi(parts[0])
end, _ := strconv.Atoi(parts[1])
for i := start; i <= end; i++ {
card = append(card, fmt.Sprintf("%s/%d", set, i))
}
}
}
} else {
card = append(card, fmt.Sprintf("%s/%s", set, strings.Split(line, " ")[0]))
}
// Are there extra arguments?
if len(strings.Split(line, " ")) == 2 {
// foil shortcut
if strings.Split(line, " ")[1] == "f" {
foil = true
}
// amount shortcut
if amount, err := strconv.Atoi(strings.Split(line, " ")[1]); err == nil {
if amount > 1 {
count = int64(amount)
}
}
}
addCards(card, unique, count)
}
}
func addCards(cards []string, unique bool, count int64) error {
client := storageConnect()
coll := &Collection{client.Database("serra").Collection("cards")}
l := Logger()
defer storageDisconnect(client)
// Loop over different cards
for _, card := range cards {
// Extract collector number and set name from card input & trim any leading 0 from collector number
if !strings.Contains(card, "/") {
l.Errorf("Invalid card format %s. Needs to be set/collector number i.e. \"usg/13\"", card)
continue
}
setName := strings.ToLower(strings.Split(card, "/")[0])
collectorNumber := strings.TrimLeft(strings.Split(card, "/")[1], "0")
if collectorNumber == "" {
l.Errorf("Invalid card format %s. Needs to be set/collector number i.e. \"usg/13\"", card)
continue
}
// Check if card is already in collection
co, err := coll.storageFind(bson.D{{"set", setName}, {"collectornumber", collectorNumber}}, bson.D{}, 0, 0)
if err != nil {
l.Error(err)
continue
}
if len(co) >= 1 {
c := co[0]
outputColor := coloredValue(c.getValue(foil))
if unique {
l.Warnf("%dx \"%s\" (%s, %s%.2f%s%s) not added, because it already exists", count, c.Name, c.Rarity, outputColor, c.getValue(foil), getCurrency(), Reset)
continue
}
if askConfirmation(&c) {
modifyCardCount(coll, &c, count, foil)
}
} else {
// Fetch card from scryfall
c, err := fetchCard(setName, collectorNumber)
outputColor := coloredValue(c.getValue(foil))
if err != nil {
l.Warn(err)
continue
}
// Write card to mongodb
var total int64 = 0
if foil {
c.SerraCountFoil = count
c.SerraCountFoilDeck = 0
total = c.SerraCountFoil
} else {
c.SerraCount = count
c.SerraCountDeck = 0
total = c.SerraCount
}
if !askConfirmation(c) {
continue
}
err = coll.storageAdd(c)
if err != nil {
l.Warn(err)
continue
}
// Give feedback of successfully added card
if foil {
l.Infof("%dx \"%s\" (%s, %s%.2f%s%s, foil) added", total, c.Name, c.Rarity, outputColor, c.getValue(foil), getCurrency(), Reset)
} else {
l.Infof("%dx \"%s\" (%s, %s%.2f%s%s) added", total, c.Name, c.Rarity, outputColor, c.getValue(foil), getCurrency(), Reset)
}
}
}
storageDisconnect(client)
return nil
}

View File

@ -1,243 +0,0 @@
package serra
import (
"bytes"
"encoding/base64"
"fmt"
"image/png"
"os"
"sort"
"strings"
"github.com/dolmen-go/kittyimg"
"github.com/nfnt/resize"
"github.com/spf13/cobra"
"go.mongodb.org/mongo-driver/bson"
)
func init() {
cardCmd.Flags().StringVarP(&artist, "artist", "a", "", "Filter by name of artist")
cardCmd.Flags().StringVarP(&rarity, "rarity", "r", "", "Filter by rarity of cards (mythic, rare, uncommon, common)")
cardCmd.Flags().StringVarP(&set, "set", "e", "", "Filter by set code (usg/mmq/vow)")
cardCmd.Flags().StringVarP(&sortby, "sort", "s", "name", "How to sort cards (value/number/name/added)")
cardCmd.Flags().StringVarP(&name, "name", "n", "", "Name of the card (regex compatible)")
cardCmd.Flags().Int64VarP(&cmc, "cmc", "m", -1, "Cumulative mana cost of card")
cardCmd.Flags().StringVarP(&color, "color", "i", "", "Color identity of card (w,u,b,r,g)")
cardCmd.Flags().StringVarP(&oracle, "oracle", "o", "", "Contains string in card text")
cardCmd.Flags().StringVarP(&cardType, "type", "t", "", "Contains string in card type line")
cardCmd.Flags().Int64VarP(&count, "min-count", "c", 0, "Occource more than X in your collection")
cardCmd.Flags().BoolVarP(&detail, "detail", "d", false, "Show details for cards (url)")
cardCmd.Flags().BoolVarP(&reserved, "reserved", "w", false, "If card is on reserved list")
cardCmd.Flags().BoolVarP(&foil, "foil", "f", false, "If card is foil list")
cardCmd.Flags().BoolVarP(&drawImg, "image", "g", false, "Draw card image using kitty format")
cardCmd.Flags().BoolVarP(&omitInDeck, "omit-in-deck", "q", false, "Omit cards that are in decks")
rootCmd.AddCommand(cardCmd)
}
var cardCmd = &cobra.Command{
Aliases: []string{"cards"},
Use: "card [card]",
Short: "Search & show cards from your collection",
Long: `Search and show cards from your collection.
If you directly put a card as an argument, it will be displayed
otherwise you'll get a list of cards as a search result.`,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, cards []string) error {
if len(cards) == 0 {
cardList := Cards(rarity, set, sortby, name, oracle, cardType, reserved, foil, 0, 0, omitInDeck)
showCardList(cardList, detail)
} else {
ShowCard(cards)
}
return nil
},
}
func ShowCard(cardids []string) {
client := storageConnect()
coll := &Collection{client.Database("serra").Collection("cards")}
l := Logger()
defer storageDisconnect(client)
for _, v := range cardids {
if len(strings.Split(v, "/")) < 2 || strings.Split(v, "/")[1] == "" {
l.Warnf("Invalid card %s", v)
continue
}
cards, _ := coll.storageFind(bson.D{{"set", strings.Split(v, "/")[0]}, {"collectornumber", strings.Split(v, "/")[1]}}, bson.D{{"name", 1}}, 0, 0)
for _, card := range cards {
showCardDetails(&card)
}
}
}
func Cards(rarity, set, sortby, name, oracle, cardType string, reserved, foil bool, skip, limit int64, omitInDeck bool) []Card {
client := storageConnect()
coll := &Collection{client.Database("serra").Collection("cards")}
defer storageDisconnect(client)
filter := bson.D{}
switch rarity {
case "uncommon":
filter = append(filter, bson.E{"rarity", "uncommon"})
case "common":
filter = append(filter, bson.E{"rarity", "common"})
case "rare":
filter = append(filter, bson.E{"rarity", "rare"})
case "mythic":
filter = append(filter, bson.E{"rarity", "mythic"})
}
var sortStage bson.D
switch sortby {
case "value":
if getCurrency() == EUR {
sortStage = bson.D{{"prices.eur", 1}}
} else {
sortStage = bson.D{{"prices.usd", 1}}
}
case "number":
sortStage = bson.D{{"collectornumber", 1}}
case "name":
sortStage = bson.D{{"name", 1}}
case "added":
sortStage = bson.D{{"serra_created", 1}}
default:
sortStage = bson.D{{"name", 1}}
}
if len(set) > 0 {
filter = append(filter, bson.E{"set", set})
}
if len(name) > 0 {
filter = append(filter, bson.E{"name", bson.D{{"$regex", ".*" + name + ".*"}, {"$options", "i"}}})
}
if len(artist) > 0 {
filter = append(filter, bson.E{"artist", bson.D{{"$regex", ".*" + artist + ".*"}, {"$options", "i"}}})
}
if cmc > -1 {
filter = append(filter, bson.E{"cmc", cmc})
}
if len(oracle) > 0 {
filter = append(filter, bson.E{"oracletext", bson.D{{"$regex", ".*" + oracle + ".*"}, {"$options", "i"}}})
}
if len(cardType) > 0 {
filter = append(filter, bson.E{"typeline", bson.D{{"$regex", ".*" + cardType + ".*"}, {"$options", "i"}}})
}
if len(color) > 0 {
colorArr := strings.Split(strings.ToUpper(color), ",")
filter = append(filter, bson.E{"coloridentity", colorArr})
}
if reserved {
filter = append(filter, bson.E{"reserved", true})
}
if foil {
filter = append(filter, bson.E{"serra_count_foil", bson.D{{"$gt", 0}}})
}
if omitInDeck {
filter = append(filter, bson.E{ "$expr", bson.D{{"$or",bson.A{
bson.D{{"$ne", bson.A{"$serra_count", "$serra_count_deck"}}},
bson.D{{"$ne", bson.A{"$serra_count_foil", "$serra_count_foil_deck"}}},
}}}})
}
cards, _ := coll.storageFind(filter, sortStage, skip, limit)
// This is needed because collectornumbers are strings (ie. "23a") but still we
// want it to be sorted numerically ... 1,2,3,10,11,100.
if sortby == "number" {
sort.Slice(cards, func(i, j int) bool {
return filterForDigits(cards[i].CollectorNumber) < filterForDigits(cards[j].CollectorNumber)
})
}
// filter out cards that do not reach the minimum amount (--min-count)
// this is done after query result because find query constructed does not support
// aggregating fields (of count and countFoil).
temp := cards[:0]
for _, card := range cards {
if (card.SerraCount + card.SerraCountFoil) >= count {
temp = append(temp, card)
}
}
cards = temp
return cards
}
func showCardList(cards []Card, detail bool) {
var total float64
if drawImg {
for _, card := range cards {
drawImage(&card)
}
} else if detail {
for _, card := range cards {
fmt.Printf("* %dx %s%s%s (%s/%s) %s%.2f%s %s %s %s\n", card.SerraCount+card.SerraCountFoil+card.SerraCountEtched, Purple, card.Name, Reset, card.Set, card.CollectorNumber, Yellow, card.getValue(false), getCurrency(), Background, strings.Replace(card.ScryfallURI, "?utm_source=api", "", 1), Reset)
total = total + card.getValue(false)*float64(card.SerraCount) + card.getValue(true)*float64(card.SerraCountFoil)
}
} else {
for _, card := range cards {
fmt.Printf("* %dx (%dx) %s%s%s (%s/%s) %s%.2f%s%s\n",
card.SerraCount+card.SerraCountFoil+card.SerraCountEtched-card.SerraCountDeck-card.SerraCountFoilDeck-card.SerraCountEtchedDeck,
card.SerraCount+card.SerraCountFoil+card.SerraCountEtched,
Purple, card.Name, Reset, card.Set, card.CollectorNumber, Yellow, card.getValue(false), getCurrency(), Reset)
total = total + card.getValue(false)*float64(card.SerraCount) + card.getValue(true)*float64(card.SerraCountFoil)
}
}
if !drawImg {
fmt.Printf("\nTotal Value: %s%.2f%s%s\n", Yellow, total, getCurrency(), Reset)
}
}
func showCardDetails(card *Card) error {
if drawImg {
drawImage(card)
} else {
fmt.Printf("%s%s%s (%s/%s)\n", Purple, card.Name, Reset, card.Set, card.CollectorNumber)
fmt.Printf("Available: %d / %d\n", card.SerraCount + card.SerraCountFoil - card.SerraCountDeck - card.SerraCountFoilDeck, card.SerraCount + card.SerraCountFoil)
fmt.Printf("Added: %s\n", stringToTime(card.SerraCreated))
fmt.Printf("Rarity: %s\n", card.Rarity)
fmt.Printf("Scryfall: %s\n", strings.Replace(card.ScryfallURI, "?utm_source=api", "", 1))
fmt.Printf("\n%sCurrent Value%s\n", Green, Reset)
fmt.Printf("* Normal: %dx %s%.2f%s%s\n", card.SerraCount, Yellow, card.getValue(false), getCurrency(), Reset)
if card.SerraCountFoil > 0 {
fmt.Printf("* Foil: %dx %s%.2f%s%s\n", card.SerraCountFoil, Yellow, card.getValue(true), getCurrency(), Reset)
}
fmt.Printf("\n%sValue History%s\n", Green, Reset)
showPriceHistory(card.SerraPrices, "* ", false)
fmt.Println()
}
return nil
}
func drawImage(card *Card) {
fmt.Printf("%s - %s (%s/%s)\n", card.Name, card.SetName, card.Set, card.CollectorNumber)
data, err := base64.StdEncoding.DecodeString(card.SerraImage64)
if err == nil {
reader := bytes.NewReader(data)
img, err := png.Decode(reader)
img = resize.Resize(305, 425, img, resize.Bicubic)
if err == nil {
kittyimg.Fprintln(os.Stdout, img)
}
}
fmt.Println()
}

View File

@ -1,77 +0,0 @@
package serra
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"go.mongodb.org/mongo-driver/bson"
)
func init() {
checkCmd.Flags().StringVarP(&set, "set", "s", "", "Filter by set code (usg/mmq/vow)")
checkCmd.Flags().BoolVarP(&detail, "detail", "d", false, "Show details for cards (url)")
rootCmd.AddCommand(checkCmd)
}
var checkCmd = &cobra.Command{
Aliases: []string{"c"},
Use: "check",
Short: "Check if a card is in your collection",
Long: "Check if a card is in your collection. Useful for list comparsions",
SilenceErrors: true,
RunE: func(cmd *cobra.Command, cards []string) error {
checkCards(cards, detail)
return nil
},
}
func checkCards(cards []string, detail bool) error {
client := storageConnect()
coll := &Collection{client.Database("serra").Collection("cards")}
defer storageDisconnect(client)
l := Logger()
// Loop over different cards
for _, card := range cards {
if !strings.Contains(card, "/") {
l.Errorf("Invalid card format %s. Needs to be set/collector number i.e. \"usg/13\"", card)
continue
}
// Extract collector number and set name from card input & trim any leading 0 from collector number
collectorNumber := strings.TrimLeft(strings.Split(card, "/")[1], "0")
setName := strings.ToLower(strings.Split(card, "/")[0])
if collectorNumber == "" {
l.Errorf("Invalid card format %s. Needs to be set/collector number i.e. \"usg/13\"", card)
continue
}
// Check if card is already in collection
co, err := coll.storageFind(bson.D{{"set", setName}, {"collectornumber", collectorNumber}}, bson.D{}, 0, 0)
if err != nil {
l.Warn(err)
continue
}
// If Card is in collection, print yes.
if len(co) >= 1 {
c := co[0]
fmt.Printf("PRESENT %s \"%s\" (%s, %.2f%s) %s\n", card, c.Name, c.Rarity, c.getValue(foil), getCurrency(), strings.Replace(c.ScryfallURI, "?utm_source=api", "", 1))
continue
} else {
if detail {
// fetch card from scyrfall if --detail was given
c, _ := fetchCard(setName, collectorNumber)
fmt.Printf("MISSING %s \"%s\" (%s, %.2f%s) %s\n", card, c.Name, c.Rarity, c.getValue(foil), getCurrency(), strings.Replace(c.ScryfallURI, "?utm_source=api", "", 1))
} else {
// Just print, the card name was not found
fmt.Printf("MISSING \"%s\"\n", card)
}
}
}
storageDisconnect(client)
return nil
}

View File

@ -1,67 +0,0 @@
package serra
import (
"strings"
"github.com/spf13/cobra"
"go.mongodb.org/mongo-driver/bson"
)
func init() {
deckCmd.Flags().Int64VarP(&count, "count", "c", 1, "Amount of cards to add")
deckCmd.Flags().BoolVarP(&foil, "foil", "f", false, "Add foil variant of card")
rootCmd.AddCommand(deckCmd)
}
var deckCmd = &cobra.Command{
Aliases: []string{"d"},
Use: "deck",
Short: "Mark a card as in a deck",
Long: "Mark a card as in a deck",
SilenceErrors: true,
RunE: func(cmd *cobra.Command, cards []string) error {
deckCards(cards, count)
return nil
},
}
func deckCards(cards []string, count int64) error {
client := storageConnect()
coll := &Collection{client.Database("serra").Collection("cards")}
l := Logger()
defer storageDisconnect(client)
// Loop over different cards
for _, card := range cards {
// Extract collector number and set name from card input & trim any leading 0 from collector number
if !strings.Contains(card, "/") {
l.Errorf("Invalid card format %s. Needs to be set/collector number i.e. \"usg/13\"", card)
continue
}
setName := strings.ToLower(strings.Split(card, "/")[0])
collectorNumber := strings.TrimLeft(strings.Split(card, "/")[1], "0")
if collectorNumber == "" {
l.Errorf("Invalid card format %s. Needs to be set/collector number i.e. \"usg/13\"", card)
continue
}
// Check if card is already in collection
co, err := coll.storageFind(bson.D{{"set", setName}, {"collectornumber", collectorNumber}}, bson.D{}, 0, 0)
if err != nil {
l.Error(err)
continue
}
if len(co) >= 1 {
modifyCardDeckCount(coll, &co[0], count, foil)
} else {
l.Errorf("Card not in collection: %s", card)
continue
}
}
storageDisconnect(client)
return nil
}

View File

@ -1,33 +0,0 @@
package serra
import (
"os"
)
const EUR = "€"
const USD = "$"
func getMongoDBURI() string {
l := Logger()
uri := os.Getenv("MONGODB_URI")
if uri == "" {
l.Fatal("You must set 'MONGODB_URI' environmental variable. See\n\t https://docs.mongodb.com/drivers/go/current/usage-examples/#environment-variable")
}
return uri
}
// Returns configured human readable name for
// the configured currency of the user
func getCurrency() string {
l := Logger()
switch os.Getenv("SERRA_CURRENCY") {
case "EUR":
return EUR
case "USD":
return USD
default:
l.Warn("You did not configure SERRA_CURRENCY. Assuming \"USD\"")
return "$"
}
}

View File

@ -1,127 +0,0 @@
package serra
import (
"encoding/csv"
"encoding/json"
"fmt"
"log"
"os"
"github.com/spf13/cobra"
)
func init() {
exportCmd.Flags().StringVarP(&set, "set", "e", "", "Filter by set code (usg/mmq/vow)")
exportCmd.Flags().StringVarP(&format, "format", "f", "tcgpowertools", "Choose format to export (tcgpowertools/json)")
exportCmd.Flags().Int64VarP(&count, "min-count", "c", 0, "Occource more than X in your collection")
rootCmd.AddCommand(exportCmd)
}
var exportCmd = &cobra.Command{
Use: "export",
Short: "Export cards from your collection",
Long: `Export cards from your collection.
Your data. Your choice.
Supports multiple output formats depending on where you want to export your collection.`,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
cardList := Cards(rarity, set, sortby, name, oracle, cardType, reserved, foil, 0, 0, false)
// filter out cards that do not reach the minimum amount (--min-count)
// this is done after query result because find query constructed does not support
// aggregating fields (of count and countFoil).
temp := cardList[:0]
for _, card := range cardList {
if (card.SerraCount + card.SerraCountFoil) >= count {
temp = append(temp, card)
}
}
cardList = temp
switch format {
case "tcgpowertools":
exportTCGPowertools(cardList)
case "tcghome":
exportTCGHome(cardList)
case "moxfield":
exportMoxfield(cardList)
case "json":
exportJSON(cardList)
}
return nil
},
}
func exportTCGPowertools(cards []Card) {
// TCGPowertools.com Example
// idProduct,quantity,name,set,condition,language,isFoil,isPlayset,isSigned,isFirstEd,price,comment
// 260009,1,Totally Lost,Gatecrash,GD,English,true,true,,,1000,
// 260009,1,Totally Lost,Gatecrash,NM,English,true,true,,,1000,
fmt.Println("quantity,cardmarketId,name,set,condition,language,isFoil,isPlayset,price,comment")
for _, card := range cards {
fmt.Printf("%d,%.0f,%s,%s,EX,German,false,false,%.2f,\n", card.SerraCount+card.SerraCountFoil, card.CardmarketID, card.Name, card.SetName, card.getValue(false))
}
}
func exportMoxfield(cards []Card) {
// Structure
// https://www.moxfield.com/help/importing-collection
records := [][]string{{
"Count", "Name", "Edition", "Condition", "Language", "Foil", "Collector Number", "Alter", "Proxy", "Purchase Price"}}
w := csv.NewWriter(os.Stdout)
for _, card := range cards {
records = append(records,
[]string{fmt.Sprintf("%d", card.SerraCount+card.SerraCountFoil), card.Name, card.Set, "NM", "English", "FALSE", card.CollectorNumber, "FALSE", "FALSE", ""})
}
for _, record := range records {
if err := w.Write(record); err != nil {
log.Fatalln("error writing record to csv:", err)
}
}
w.Flush()
if err := w.Error(); err != nil {
log.Fatal(err)
}
}
func exportTCGHome(cards []Card) {
// Strucutre
// https://app.tcg-home.com/e686ea62-7078-4f52-bd6f-515e18c7dc6a
records := [][]string{{
"amount", "name", "finish", "set", "collector_number", "language", "condition", "scryfall_id", "purchase_price"}}
w := csv.NewWriter(os.Stdout)
for _, card := range cards {
records = append(records,
[]string{fmt.Sprintf("%d", card.SerraCount+card.SerraCountFoil), card.Name, "", card.Set, card.CollectorNumber, "English", "EX", card.ID, ""})
}
for _, record := range records {
if err := w.Write(record); err != nil {
log.Fatalln("error writing record to csv:", err)
}
}
w.Flush()
if err := w.Error(); err != nil {
log.Fatal(err)
}
}
func exportJSON(cards []Card) {
ehj, _ := json.MarshalIndent(cards, "", " ")
fmt.Println(string(ehj))
}

View File

@ -1,323 +0,0 @@
package serra
import (
"errors"
"fmt"
"math"
"os"
"strconv"
"time"
"unicode"
"github.com/charmbracelet/log"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type Rarities struct {
Rares, Uncommons, Commons, Mythics float64
}
var (
Icon = "\U0001F9D9\U0001F3FC"
Reset = "\033[0m"
Background = "\033[38;5;59m"
CurrentLine = "\033[38;5;60m"
Foreground = "\033[38;5;231m"
Comment = "\033[38;5;103m"
Cyan = "\033[38;5;159m"
Green = "\033[38;5;120m"
Orange = "\033[38;5;222m"
Pink = "\033[38;5;212m"
Purple = "\033[38;5;183m"
Red = "\033[38;5;210m"
Yellow = "\033[38;5;229m"
)
func Logger() *log.Logger {
l := log.New(os.Stderr)
l.SetReportTimestamp(false)
return l
}
func getStoredCard(coll *Collection, c *Card) (Card, error) {
// find already existing card
sort := bson.D{{"_id", 1}}
searchFilter := bson.D{{"_id", c.ID}}
storedCards, err := coll.storageFind(searchFilter, sort, 0, 0)
if err != nil {
return Card{}, err
}
return storedCards[0], nil
}
func modifyCardCount(coll *Collection, c *Card, amount int64, foil bool) error {
l := Logger()
storedCard, err := getStoredCard(coll, c)
if err != nil {
return err
}
// update card amount
var update bson.M
if foil {
update = bson.M{
"$set": bson.M{"serra_count_foil": storedCard.SerraCountFoil + amount},
}
} else {
update = bson.M{
"$set": bson.M{"serra_count": storedCard.SerraCount + amount},
}
}
coll.storageUpdate(bson.M{"_id": bson.M{"$eq": c.ID}}, update)
var total int64
if foil {
total = storedCard.SerraCountFoil + amount
if amount < 0 {
l.Warnf("Reduced card amount of \"%s\" (%.2f%s, foil) from %d to %d", storedCard.Name, storedCard.getValue(true), getCurrency(), storedCard.SerraCountFoil, total)
} else {
l.Warnf("Increased card amount of \"%s\" (%.2f%s, foil) from %d to %d", storedCard.Name, storedCard.getValue(true), getCurrency(), storedCard.SerraCountFoil, total)
}
} else {
total = storedCard.SerraCount + amount
if amount < 0 {
l.Warnf("Reduced card amount of \"%s\" (%.2f%s) from %d to %d", storedCard.Name, storedCard.getValue(false), getCurrency(), storedCard.SerraCount, total)
} else {
l.Warnf("Increased card amount of \"%s\" (%.2f%s) from %d to %d", storedCard.Name, storedCard.getValue(false), getCurrency(), storedCard.SerraCount, total)
}
}
return nil
}
func modifyCardDeckCount(coll *Collection, c *Card, amount int64, foil bool) error {
l := Logger()
storedCard, err := getStoredCard(coll, c)
if err != nil {
return err
}
// update card amount
var update bson.M
if foil {
newAmount := min(max(storedCard.SerraCountFoilDeck + amount, 0), storedCard.SerraCountFoil)
l.Infof("%d / %d available", storedCard.SerraCountFoil - newAmount, storedCard.SerraCountFoil)
update = bson.M{
"$set": bson.M{"serra_count_foil_deck": newAmount},
}
} else {
newAmount := min(max(storedCard.SerraCountDeck + amount, 0), storedCard.SerraCount)
l.Infof("%d / %d available", storedCard.SerraCount - newAmount, storedCard.SerraCount)
update = bson.M{
"$set": bson.M{"serra_count_deck": newAmount},
}
}
coll.storageUpdate(bson.M{"_id": bson.M{"$eq": c.ID}}, update)
return nil
}
func findCardByCollectorNumber(coll *Collection, setCode string, collectorNumber string) (*Card, error) {
sort := bson.D{{"_id", 1}}
searchFilter := bson.D{{"set", setCode}, {"collectornumber", collectorNumber}}
storedCards, err := coll.storageFind(searchFilter, sort, 0, 0)
if err != nil {
return &Card{}, err
}
if len(storedCards) < 1 {
return &Card{}, errors.New("Card not found")
}
return &storedCards[0], nil
}
func stringToTime(s primitive.DateTime) string {
return time.UnixMilli(int64(s)).Format("2006-01-02")
}
// missing compares two slices and returns slice of differences
func missing(a, b []string) []string {
type void struct{}
// create map with length of the 'a' slice
ma := make(map[string]void, len(a))
diffs := []string{}
// Convert first slice to map with empty struct (0 bytes)
for _, ka := range a {
ma[ka] = void{}
}
// find missing values in a
for _, kb := range b {
if _, ok := ma[kb]; !ok {
diffs = append(diffs, kb)
}
}
return diffs
}
func findSetByCode(coll *Collection, setcode string) (*Set, error) {
storedSets, err := coll.storageFindSet(bson.D{{"code", setcode}}, bson.D{{"_id", 1}})
if err != nil {
return &Set{}, err
}
if len(storedSets) < 1 {
return &Set{}, errors.New("Set not found")
}
return &storedSets[0], nil
}
func convertManaSymbols(sym []interface{}) string {
var mana string
if len(sym) == 0 {
mana = mana + "None" //probibited sign for lands
}
for _, v := range sym {
switch v {
case "B":
mana = mana + "Black" //black
case "R":
mana = mana + "Red" //red
case "G":
mana = mana + "Green" //green
case "U":
mana = mana + "Blue" //blue
case "W":
mana = mana + "White" //white
}
}
return mana
}
func convertRarities(rar []primitive.M) Rarities {
// this is maybe the ugliest way someone could choose to verify, if a rarity type is missing
// [
// { _id: { rarity: 'common' }, count: 20 },
// { _id: { rarity: 'uncommon' }, count: 2 }
// ]
// if a result like this is there, 1 rarity type "rare" is not in the array. and needs to be
// initialized with 0, otherwise we get a panic
var ri Rarities
for _, r := range rar {
switch r["_id"] {
case "rare":
ri.Rares = r["count"].(float64)
case "uncommon":
ri.Uncommons = r["count"].(float64)
case "common":
ri.Commons = r["count"].(float64)
case "mythic":
ri.Mythics = r["count"].(float64)
}
}
return ri
}
func showPriceHistory(prices []PriceEntry, prefix string, total bool) {
var before float64
for _, e := range prices {
var value float64
if total {
if getCurrency() == EUR {
value = e.Eur + e.EurFoil
} else {
value = e.Usd + e.UsdFoil
}
} else {
if getCurrency() == EUR {
value = e.Eur
} else {
value = e.Usd
}
}
if value > before && before != 0 {
fmt.Printf("%s%s%s %.2f%s%s (%+.2f%%, %+.2f%s)\n", prefix, stringToTime(e.Date), Green, value, getCurrency(), Reset, (value/before*100)-100, value-before, getCurrency())
} else if value < before {
fmt.Printf("%s%s%s %.2f%s%s (%+.2f%%, %+.2f%s)\n", prefix, stringToTime(e.Date), Red, value, getCurrency(), Reset, (value/before*100)-100, value-before, getCurrency())
} else {
fmt.Printf("%s%s %.2f%s%s\n", prefix, stringToTime(e.Date), value, getCurrency(), Reset)
}
before = value
}
}
func filterForDigits(str string) int {
var numStr string
for _, c := range str {
if unicode.IsDigit(c) {
numStr += string(c)
}
}
s, _ := strconv.Atoi(numStr)
return s
}
func getFloat64(unknown interface{}) (float64, error) {
switch i := unknown.(type) {
case float64:
return i, nil
case float32:
return float64(i), nil
case int64:
return float64(i), nil
case int32:
return float64(i), nil
case int:
return float64(i), nil
case uint64:
return float64(i), nil
case uint32:
return float64(i), nil
case uint:
return float64(i), nil
default:
return math.NaN(), errors.New("non-numeric type could not be converted to float")
}
}
func coloredValue(value float64) string {
outputColor := Reset
if value > 1 {
outputColor = Green
}
if value > 5 {
outputColor = Yellow
}
if value > 10 {
outputColor = Red
}
return outputColor
}
func askConfirmation(card *Card) bool {
drawImage(card)
fmt.Println("Is this correct (y/n)?")
var char = 'x'
for char != 'y' && char != 'n' {
fmt.Scanf("%c\n", &char)
}
return char == 'y'
}

View File

@ -1,84 +0,0 @@
package serra
import (
"fmt"
"sort"
"strconv"
"github.com/spf13/cobra"
"go.mongodb.org/mongo-driver/bson"
)
func init() {
rootCmd.AddCommand(missingCmd)
}
var missingCmd = &cobra.Command{
Aliases: []string{"m"},
Use: "missing <set>",
Short: "Display missing cards from a set",
Long: `In case you are a set collector, you can generate a list of
cards you dont own (yet) :)`,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, setName []string) error {
client := storageConnect()
coll := &Collection{client.Database("serra").Collection("cards")}
l := Logger()
defer storageDisconnect(client)
// fetch all cards in set
cards, err := coll.storageFind(bson.D{{"set", setName[0]}}, bson.D{{"collectornumber", 1}}, 0, 0)
if (err != nil) || len(cards) == 0 {
l.Errorf("Set %s not found or no card in your collection.", setName[0])
return err
}
// fetch set informations
setcoll := &Collection{client.Database("serra").Collection("sets")}
sets, _ := setcoll.storageFindSet(bson.D{{"code", setName[0]}}, bson.D{{"_id", 1}})
set := sets[0]
fmt.Printf("Missing cards in %s\n", sets[0].Name)
// generate set with all setnumbers
var (
completeSet []string
i int64
)
for i = 1; i <= set.CardCount; i++ {
completeSet = append(completeSet, strconv.FormatInt(i, 10))
}
// iterate over all cards in collection
var inCollection []string
for _, c := range cards {
inCollection = append(inCollection, c.CollectorNumber)
}
misses := missing(inCollection, completeSet)
// Fetch all missing cards
missingCards := []*Card{}
for _, m := range misses {
card, err := fetchCard(setName[0], m)
if err != nil {
continue
}
missingCards = append(missingCards, card)
}
// Sort the missing cards by ID
sort.Slice(missingCards, func(i, j int) bool {
id1, _ := strconv.Atoi(missingCards[i].CollectorNumber)
id2, _ := strconv.Atoi(missingCards[j].CollectorNumber)
return id1 < id2
})
for _, card := range missingCards {
fmt.Printf("%s%s/%s\t%s(%s, %shttps://scryfall.com/card/%s/%s%s)\t%s%.02f%s%s\t%s (%s)\n", Purple, card.Set, card.CollectorNumber, Reset, string([]rune(card.Rarity)[0]), Background, card.Set, card.CollectorNumber, Reset, Green, card.getValue(false), Reset, getCurrency(), card.Name, card.SetName)
}
return nil
},
}

View File

@ -1,52 +0,0 @@
// Package serra
//
// It implements base functions and also cli wrappers
// The entire tool consists only of this one package.
package serra
import (
"github.com/spf13/cobra"
)
var (
Version = "unknown"
address string
artist string
cardType string
color string
cmc int64
count int64
detail bool
drawImg bool
foil bool
format string
interactive bool
limit float64
name string
omitInDeck bool
oracle string
port uint64
rarity string
reserved bool
set string
sinceBeginning bool
sinceLastUpdate bool
sortby string
unique bool
)
var rootCmd = &cobra.Command{
Version: Version,
Long: `serra - Magic: The Gathering Collection Tracker`,
Use: "serra",
DisableFlagsInUseLine: true,
SilenceErrors: true,
}
func Execute() {
l := Logger()
if err := rootCmd.Execute(); err != nil {
l.Fatal(err)
}
}

View File

@ -1,245 +0,0 @@
package serra
import (
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cobra"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
func init() {
rootCmd.AddCommand(statsCmd)
}
var statsCmd = &cobra.Command{
Aliases: []string{"stats"},
Use: "stats",
Short: "Shows statistics of the collection",
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
Stats()
return nil
},
}
func Stats() {
client := storageConnect()
coll := &Collection{client.Database("serra").Collection("cards")}
totalcoll := &Collection{client.Database("serra").Collection("total")}
defer storageDisconnect(client)
// Show Value Stats
showValueStats(coll, totalcoll)
// Reserved List
showReservedListStats(coll)
// Rarities
showRarityStats(coll)
// Colors
showColorStats(coll)
// Artists
showArtistStats(coll)
// Mana Curve of Collection
showManaCurveStats(coll)
// Show cards added per month
showCardsAddedPerMonth(coll)
}
func showValueStats(coll *Collection, totalcoll *Collection) {
l := Logger()
// Value and Card Numbers
stats, _ := coll.storageAggregate(mongo.Pipeline{
bson.D{
{"$group", bson.D{
{"_id", nil},
{"value", bson.D{{"$sum", bson.D{{"$multiply", bson.A{getCurrencyField(false), "$serra_count"}}}}}},
{"value_foil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{getCurrencyField(true), "$serra_count_foil"}}}}}},
{"count", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count"}}}}}},
{"count_foil", bson.D{{"$sum", "$serra_count_foil"}}},
{"rarity", bson.D{{"$sum", "$rarity"}}},
{"unique", bson.D{{"$sum", 1}}},
}},
},
bson.D{
{"$addFields", bson.D{
{"count_all", bson.D{{"$sum", bson.A{"$count", "$count_foil"}}}},
}},
},
})
fmt.Printf("%sCards %s\n", Green, Reset)
fmt.Printf("Total: %s%.0f%s\n", Yellow, stats[0]["count_all"], Reset)
fmt.Printf("Unique: %s%d%s\n", Purple, stats[0]["unique"], Reset)
fmt.Printf("Normal: %s%.0f%s\n", Purple, stats[0]["count"], Reset)
fmt.Printf("Foil: %s%d%s\n", Purple, stats[0]["count_foil"], Reset)
// Total Value
fmt.Printf("\n%sTotal Value%s\n", Green, Reset)
normalValue, err := getFloat64(stats[0]["value"])
if err != nil {
l.Error(err)
normalValue = 0
}
foilValue, err := getFloat64(stats[0]["value_foil"])
if err != nil {
l.Error(err)
foilValue = 0
}
countAll, err := getFloat64(stats[0]["count_all"])
if err != nil {
l.Error(err)
foilValue = 0
}
totalValue := normalValue + foilValue
fmt.Printf("Total: %s%.2f%s%s\n", Pink, totalValue, getCurrency(), Reset)
fmt.Printf("Normal: %s%.2f%s%s\n", Pink, normalValue, getCurrency(), Reset)
fmt.Printf("Foils: %s%.2f%s%s\n", Pink, foilValue, getCurrency(), Reset)
fmt.Printf("Average Card: %s%.2f%s%s\n", Pink, totalValue/countAll, getCurrency(), Reset)
total, _ := totalcoll.storageFindTotal()
fmt.Printf("History: \n")
showPriceHistory(total.Value, "* ", true)
}
func showReservedListStats(coll *Collection) {
reserved, _ := coll.storageAggregate(mongo.Pipeline{
bson.D{
{"$match", bson.D{
{"reserved", true}}}},
bson.D{
{"$group", bson.D{
{"_id", nil},
{"count", bson.D{{"$sum", 1}}},
}}},
})
var countReserved int32
if len(reserved) > 0 {
countReserved = reserved[0]["count"].(int32)
}
fmt.Printf("Reserved List: %s%d%s\n", Yellow, countReserved, Reset)
}
func showRarityStats(coll *Collection) {
rar, _ := coll.storageAggregate(mongo.Pipeline{
bson.D{
{"$group", bson.D{
{"_id", "$rarity"},
{"count", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count"}}}}}},
}}},
bson.D{
{"$sort", bson.D{
{"_id", 1},
}}},
})
ri := convertRarities(rar)
fmt.Printf("\n%sRarity%s\n", Green, Reset)
fmt.Printf("Mythics: %s%.0f%s\n", Pink, ri.Mythics, Reset)
fmt.Printf("Rares: %s%.0f%s\n", Pink, ri.Rares, Reset)
fmt.Printf("Uncommons: %s%.0f%s\n", Yellow, ri.Uncommons, Reset)
fmt.Printf("Commons: %s%.0f%s\n", Purple, ri.Commons, Reset)
}
func showCardsAddedPerMonth(coll *Collection) {
fmt.Printf("\n%sCards added over time%s\n", Green, Reset)
type Caot struct {
ID struct {
Year int32 `mapstructure:"year"`
Month int32 `mapstructure:"month"`
} `mapstructure:"_id"`
Count int32 `mapstructure:"count"`
}
caot, _ := coll.storageAggregate(mongo.Pipeline{
bson.D{
{"$project", bson.D{
{"month", bson.D{
{"$month", "$serra_created"}}},
{"year", bson.D{
{"$year", "$serra_created"}},
}},
}},
bson.D{
{"$group", bson.D{
{"_id", bson.D{{"month", "$month"}, {"year", "$year"}}},
{"count", bson.D{{"$sum", 1}}},
}},
},
bson.D{
{"$sort", bson.D{{"_id.year", 1}, {"_id.month", 1}}},
},
})
for _, mo := range caot {
moo := new(Caot)
mapstructure.Decode(mo, moo)
fmt.Printf("%d-%02d: %s%d%s\n", moo.ID.Year, moo.ID.Month, Purple, moo.Count, Reset)
}
}
func showManaCurveStats(coll *Collection) {
cmc, _ := coll.storageAggregate(mongo.Pipeline{
bson.D{
{"$group", bson.D{
{"_id", "$cmc"},
{"count", bson.D{{"$sum", 1}}},
}}},
bson.D{
{"$sort", bson.D{
{"_id", 1},
}}},
})
fmt.Printf("\n%sMana Curve%s\n", Green, Reset)
for _, mc := range cmc {
fmt.Printf("%.0f: %s%d%s\n", mc["_id"], Purple, mc["count"], Reset)
}
}
func showArtistStats(coll *Collection) {
artists, _ := coll.storageAggregate(mongo.Pipeline{
bson.D{
{"$group", bson.D{
{"_id", "$artist"},
{"count", bson.D{{"$sum", 1}}},
}}},
bson.D{
{"$sort", bson.D{
{"count", -1},
}}},
bson.D{
{"$limit", 10}},
})
fmt.Printf("\n%sTop Artists%s\n", Green, Reset)
for _, artist := range artists {
fmt.Printf("%s: %s%d%s\n", artist["_id"].(string), Purple, artist["count"], Reset)
}
}
func showColorStats(coll *Collection) {
sets, _ := coll.storageAggregate(mongo.Pipeline{
bson.D{
{"$match", bson.D{
{"coloridentity", bson.D{{"$size", 1}}}}}},
bson.D{
{"$group", bson.D{
{"_id", "$coloridentity"},
{"count", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count"}}}}}},
}}},
bson.D{
{"$sort", bson.D{
{"count", -1},
}}},
})
fmt.Printf("\n%sColors%s\n", Green, Reset)
for _, set := range sets {
x, _ := set["_id"].(primitive.A)
s := []interface{}(x)
fmt.Printf("%s: %s%.0f%s\n", convertManaSymbols(s), Purple, set["count"], Reset)
}
}

View File

@ -1,23 +0,0 @@
package serra
import (
"github.com/spf13/cobra"
)
func init() {
undeckCmd.Flags().Int64VarP(&count, "count", "c", 1, "Amount of cards to add")
undeckCmd.Flags().BoolVarP(&foil, "foil", "f", false, "Add foil variant of card")
rootCmd.AddCommand(undeckCmd)
}
var undeckCmd = &cobra.Command{
Aliases: []string{"u"},
Use: "undeck",
Short: "Unmark a card as in a deck",
Long: "Unmark a card as in a deck",
SilenceErrors: true,
RunE: func(cmd *cobra.Command, cards []string) error {
deckCards(cards, -count)
return nil
},
}

View File

@ -1,121 +0,0 @@
package serra
import (
"net/http"
"strconv"
"text/template"
"github.com/gin-gonic/gin"
"github.com/spf13/cobra"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
func init() {
webCmd.Flags().StringVarP(&address, "address", "a", "0.0.0.0", "Address to listen on")
webCmd.Flags().Uint64VarP(&port, "port", "p", 8080, "Port to listen on")
rootCmd.AddCommand(webCmd)
}
func add(a, b int64) int64 {
return a + b
}
var webCmd = &cobra.Command{
Aliases: []string{"a"},
Use: "web",
Short: "Startup web interface",
Long: "Start a tiny web interface to have a web view of your collection",
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
startWeb()
return nil
},
}
type Query struct {
Name string `form:"name"`
Set string `form:"set"`
Sort string `form:"sort"`
Limit int64 `form:"limit"`
Page int64 `form:"page"`
}
func startWeb() error {
router := gin.Default()
router.SetFuncMap(template.FuncMap{
"add": add,
})
router.LoadHTMLGlob("templates/*.tmpl")
router.Static("/assets", "./assets")
// Landing page
router.GET("/", landingPage)
router.Run(address + ":" + strconv.FormatUint(port, 10))
return nil
}
func landingPage(c *gin.Context) {
var query Query
if c.ShouldBind(&query) == nil {
// Construct per Page results "limit"
strLimit := c.DefaultQuery("limit", "500")
limit, _ := strconv.ParseInt(strLimit, 10, 64)
if limit == 0 {
limit = 500
}
// Fetch all sets for Dropdown
sets := Sets("release")
// Fetch all results based on filter criteria
cards := Cards("", query.Set, query.Sort, query.Name, "", "", false, false, query.Page*int64(limit), limit, false)
// Construct quick way for counting results
filter := bson.D{}
client := storageConnect()
coll := &Collection{client.Database("serra").Collection("cards")}
if query.Set != "" {
filter = append(filter, bson.E{"set", query.Set})
}
if query.Name != "" {
filter = append(filter, bson.E{"name", bson.D{{"$regex", ".*" + query.Name + ".*"}, {"$options", "i"}}})
}
counts, _ := coll.storageAggregate(mongo.Pipeline{
bson.D{
{"$match", filter},
},
bson.D{
{"$group", bson.D{
{"_id", nil},
{"count", bson.D{{"$sum", 1}}},
}}},
})
defer storageDisconnect(client)
// Catch index error on no results
var numCards int32
if len(counts) != 0 {
numCards = counts[0]["count"].(int32)
}
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Serra",
"cards": cards,
"sets": sets,
"query": query,
"version": Version,
"prevPage": query.Page - 1,
"page": query.Page,
"nextPage": query.Page + 1,
"limit": limit,
"numCards": int64(numCards),
"numPages": int64(numCards) / limit,
})
}
}

View File

@ -22,12 +22,11 @@ an overview in what cards you own and what value they have.
**What Serra does not** **What Serra does not**
* Does not care about conditions (NM, M, GD...) * Does not give a shit about conditions (NM, M, GD...)
* Does not track etched cards. Only normal and foil.
# Quickstart # Quickstart
## Install Binaries ### Install Binaries
on macOS you can use on macOS you can use
@ -37,15 +36,16 @@ on Linux/BSD/Windows you can download binaries from
https://github.com/noqqe/serra/releases https://github.com/noqqe/serra/releases
## Spin up Database ### Spin up Database
To run serra, a MongoDB Database is required. The best way is to setup one by yourself. Any way it connects is fine. To run serra, a MongoDB Database is required. The best way is to setup one by yourself. Any way it connects is fine.
You can also use the docker-compose setup included in this Repo: You can also use the docker-compose setup included in this Repo:
docker-compose up -d docker-compose up -d
## Configure the Database ### Configure the Database
Configure `serra` via Environment variables Configure `serra` via Environment variables
@ -69,7 +69,6 @@ Usage:
Available Commands: Available Commands:
add Add a card to your collection add Add a card to your collection
card Search & show cards from your collection card Search & show cards from your collection
check Check if a card is in your collection
completion Generate the autocompletion script for the specified shell completion Generate the autocompletion script for the specified shell
flops What cards lost most value flops What cards lost most value
help Help about any command help Help about any command
@ -138,12 +137,6 @@ update. After all Sets are updated, the whole collection value is updated.
![](https://github.com/noqqe/serra/blob/main/imgs/update.png) ![](https://github.com/noqqe/serra/blob/main/imgs/update.png)
## Check
To add a card to your collection.
![](https://github.com/noqqe/serra/blob/main/imgs/check.png)
## Adding all those cards, manually? ## Adding all those cards, manually?
Yes. While there are serveral OCR/Photo Scanners for mtg cards, I found they Yes. While there are serveral OCR/Photo Scanners for mtg cards, I found they
@ -163,65 +156,9 @@ one> 3
1x "Apostle of Invasion" (uncommon, 0.03 USD) added to Collection. 1x "Apostle of Invasion" (uncommon, 0.03 USD) added to Collection.
``` ```
It also supports ranges of cards
```
dmr> 1-3
1x "Auramancer" (common, 0.02$) added to Collection.
1x "Battle Screech" (uncommon, 0.09$) added to Collection.
1x "Cleric of the Forward Order" (common, 0.01$) added to Collection.
```
Its basically typing 2-3 digit numbers and hitting enter. I was way faster Its basically typing 2-3 digit numbers and hitting enter. I was way faster
with this approach then Smartphone scanners. with this approach then Smartphone scanners.
# Upgrade
If you want to upgrade, go to [releases](https://github.com/noqqe/serra/releases) Page and download the corresponding release for your platform.
For example:
```
wget https://github.com/noqqe/serra/releases/download/3.10.0/serra_Darwin_x86_64.tar.gz
tar zxfv serra_Darwin_x86_64.tar.gz
./serra
```
## Upgrade Notes
### 2.x.x -> 3.x.x
No extra steps needed. Only new Webinterface and Foil support
### 1.5.3 -> 2.0.0
In this stage of the development of serra, I was breaking the original
database "schema" without migration.
Sadly you need to export the cards from the mongodb and import it again using `serra add ` commands
I wrote a little helper script in python to export all the cards in format set/number and generate some queries
```
python3 export.py > add_commands.sh
head add_commands.sh
./serra add 5ed/3 -c 1
./serra add mmq/2 -c 1
./serra add p02/4 -c 1
./serra add chr/44 -c 1
./serra add 4ed/291 -c 1
./serra add 4ed/292 -c 1
./serra add mir/2 -c 1
./serra add usg/231 -c 1
./serra add mir/155 -c 1
./serra add pcy/29 -c 2
<do the upgrade of serra (download new binary>
<delete the old mongodb or just empty it completly>
bash add_commands.sh
```
# Development # Development
## Install ## Install

View File

@ -1,15 +0,0 @@
from pymongo import MongoClient
import os
import pymongo
CONNECTION_STRING = os.getenv("MONGODB_URI")
client = MongoClient(CONNECTION_STRING+'/admin')
# Create a new collection
collection = client["serra"]["cards"]
cards=collection.find()
for c in cards:
print("./serra add %s/%s -c %s" % (c["set"], c["collectornumber"], c["serra_count"]))

View File

@ -1,19 +0,0 @@
#!/usr/bin/env fish
set SET $argv[1]
# export and remove colors
serra cards --set $SET --min-count 2 --sort value | gsed 's/\x1B[@A-Z\\\]^_]\|\x1B\[[0-9:;<=>?]*[-!"#$%&'"'"'()*+,.\/]*[][\\@A-Z^_`a-z{|}~]//g' > $SET
# edit
nvim $SET
# remove formatting
cat $SET | gsed 's/^\* //' | gsed 's/x.*(/ /' | gsed 's/).*//' | grep -v "Total Value" > {$SET}.txt
# delete everything from serra
for x in (cat {$SET}.txt) ; echo serra remove -c $x ; end
# cleanup
rm $SET
rm {$SET}.txt

View File

@ -1,13 +0,0 @@
#!/usr/bin/env bash
# This script replaces a normal card with a foil card I needed this because the
# tracking of foils was only added in version 3.5.0 of serra
# give set code as $1 like "dmr"
: ${1:?}
while true; do
read -p "$1> " card
serra add --foil ${1}/${card}
serra remove ${1}/${card}
done

View File

@ -1,7 +1,7 @@
// Package main provides a typing test // Package main provides a typing test
package main package main
import "github.com/noqqe/serra/pkg/serra" import "github.com/noqqe/serra/src/serra"
func main() { func main() {
serra.Execute() serra.Execute()

128
src/serra/add.go Normal file
View File

@ -0,0 +1,128 @@
package serra
import (
"fmt"
"os"
"strings"
"github.com/chzyer/readline"
"github.com/spf13/cobra"
"go.mongodb.org/mongo-driver/bson"
)
func init() {
addCmd.Flags().Int64VarP(&count, "count", "c", 1, "Amount of cards to add")
addCmd.Flags().BoolVarP(&unique, "unique", "u", false, "Only add card if not existent yet")
addCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Spin up interactive terminal")
addCmd.Flags().StringVarP(&set, "set", "s", "", "Filter by set code (usg/mmq/vow)")
addCmd.Flags().BoolVarP(&foil, "foil", "f", false, "Add foil variant of card")
rootCmd.AddCommand(addCmd)
}
var addCmd = &cobra.Command{
Aliases: []string{"a"},
Use: "add",
Short: "Add a card to your collection",
Long: "Adds a card from scryfall to your collection. Amount can be modified using flags",
SilenceErrors: true,
RunE: func(cmd *cobra.Command, cards []string) error {
if interactive {
addCardsInteractive(unique, set)
} else {
addCards(cards, unique, count)
}
return nil
},
}
func addCardsInteractive(unique bool, set string) {
if len(set) == 0 {
LogMessage("Error: --set must be given in interactive mode", "red")
os.Exit(1)
}
rl, err := readline.New(fmt.Sprintf("%s> ", set))
if err != nil {
panic(err)
}
defer rl.Close()
for {
line, err := rl.Readline()
if err != nil { // io.EOF
break
}
// construct card input for addCards
card := []string{}
card = append(card, fmt.Sprintf("%s/%s", set, strings.TrimSpace(line)))
addCards(card, unique, count)
}
}
func addCards(cards []string, unique bool, count int64) error {
client := storage_connect()
coll := &Collection{client.Database("serra").Collection("cards")}
defer storage_disconnect(client)
// Loop over different cards
for _, card := range cards {
// Check if card is already in collection
co, _ := coll.storage_find(bson.D{{"set", strings.Split(card, "/")[0]}, {"collectornumber", strings.Split(card, "/")[1]}}, bson.D{})
if len(co) >= 1 {
c := co[0]
if unique {
LogMessage(fmt.Sprintf("Not adding \"%s\" (%s, %.2f %s) to Collection because it already exists.", c.Name, c.Rarity, c.getValue(foil), getCurrency()), "red")
continue
}
modify_count_of_card(coll, &c, count, foil)
var total int64 = 0
if foil {
total = c.SerraCountFoil + count
} else {
total = c.SerraCount + count
}
// Give feedback of successfully added card
LogMessage(fmt.Sprintf("%dx \"%s\" (%s, %.2f %s) added to Collection.", total, c.Name, c.Rarity, c.getValue(foil), getCurrency()), "green")
// If card is not already in collection, fetching from scyfall
} else {
// Fetch card from scryfall
c, err := fetch_card(card)
if err != nil {
LogMessage(fmt.Sprintf("%v", err), "red")
continue
}
// Write card to mongodb
var total int64 = 0
if foil {
c.SerraCountFoil = count
total = c.SerraCountFoil
} else {
c.SerraCount = count
total = c.SerraCount
}
err = coll.storage_add(c)
if err != nil {
LogMessage(fmt.Sprintf("%v", err), "red")
continue
}
// Give feedback of successfully added card
LogMessage(fmt.Sprintf("%dx \"%s\" (%s, %.2f %s) added to Collection.", total, c.Name, c.Rarity, c.getValue(foil), getCurrency()), "green")
}
}
storage_disconnect(client)
return nil
}

158
src/serra/card.go Normal file
View File

@ -0,0 +1,158 @@
package serra
import (
"fmt"
"sort"
"strings"
"github.com/spf13/cobra"
"go.mongodb.org/mongo-driver/bson"
)
func init() {
cardCmd.Flags().StringVarP(&rarity, "rarity", "r", "", "Filter by rarity of cards (mythic, rare, uncommon, common)")
cardCmd.Flags().StringVarP(&set, "set", "e", "", "Filter by set code (usg/mmq/vow)")
cardCmd.Flags().StringVarP(&sortby, "sort", "s", "name", "How to sort cards (value/number/name/added)")
cardCmd.Flags().StringVarP(&name, "name", "n", "", "Name of the card (regex compatible)")
cardCmd.Flags().StringVarP(&oracle, "oracle", "o", "", "Contains string in card text")
cardCmd.Flags().StringVarP(&cardType, "type", "t", "", "Contains string in card type line")
rootCmd.AddCommand(cardCmd)
}
var cardCmd = &cobra.Command{
Aliases: []string{"cards"},
Use: "card [card]",
Short: "Search & show cards from your collection",
Long: `Search and show cards from your collection.
If you directly put a card as an argument, it will be displayed
otherwise you'll get a list of cards as a search result.`,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, cards []string) error {
if len(cards) == 0 {
card_list := Cards(rarity, set, sortby, name, oracle, cardType)
show_card_list(card_list)
} else {
ShowCard(cards)
}
return nil
},
}
func ShowCard(cardids []string) {
client := storage_connect()
coll := &Collection{client.Database("serra").Collection("cards")}
defer storage_disconnect(client)
for _, v := range cardids {
if len(strings.Split(v, "/")) < 2 || strings.Split(v, "/")[1] == "" {
LogMessage(fmt.Sprintf("Invalid card %s", v), "red")
continue
}
cards, _ := coll.storage_find(bson.D{{"set", strings.Split(v, "/")[0]}, {"collectornumber", strings.Split(v, "/")[1]}}, bson.D{{"name", 1}})
for _, card := range cards {
show_card_details(&card)
}
}
}
func Cards(rarity, set, sortby, name, oracle, cardType string) []Card {
client := storage_connect()
coll := &Collection{client.Database("serra").Collection("cards")}
defer storage_disconnect(client)
filter := bson.D{}
switch rarity {
case "uncommon":
filter = append(filter, bson.E{"rarity", "uncommon"})
case "common":
filter = append(filter, bson.E{"rarity", "common"})
case "rare":
filter = append(filter, bson.E{"rarity", "rare"})
}
var sortStage bson.D
switch sortby {
case "value":
if getCurrency() == "EUR" {
sortStage = bson.D{{"prices.eur", 1}}
} else {
sortStage = bson.D{{"prices.usd", 1}}
}
case "number":
sortStage = bson.D{{"collectornumber", 1}}
case "name":
sortStage = bson.D{{"name", 1}}
case "added":
sortStage = bson.D{{"serra_created", 1}}
default:
sortStage = bson.D{{"name", 1}}
}
if len(set) > 0 {
filter = append(filter, bson.E{"set", set})
}
if len(name) > 0 {
filter = append(filter, bson.E{"name", bson.D{{"$regex", ".*" + name + ".*"}, {"$options", "i"}}})
}
if len(oracle) > 0 {
filter = append(filter, bson.E{"oracletext", bson.D{{"$regex", ".*" + oracle + ".*"}, {"$options", "i"}}})
}
if len(cardType) > 0 {
filter = append(filter, bson.E{"typeline", bson.D{{"$regex", ".*" + cardType + ".*"}, {"$options", "i"}}})
}
cards, _ := coll.storage_find(filter, sortStage)
// This is needed because collectornumbers are strings (ie. "23a") but still we
// want it to be sorted numerically ... 1,2,3,10,11,100.
if sortby == "number" {
sort.Slice(cards, func(i, j int) bool {
return filterForDigits(cards[i].CollectorNumber) < filterForDigits(cards[j].CollectorNumber)
})
}
return cards
}
func show_card_list(cards []Card) {
var total float64
for _, card := range cards {
LogMessage(fmt.Sprintf("* %dx %s%s%s (%s/%s) %s%.2f %s%s", card.SerraCount+card.SerraCountFoil+card.SerraCountEtched, Purple, card.Name, Reset, card.Set, card.CollectorNumber, Yellow, card.getValue(false), getCurrency(), Reset), "normal")
total = total + card.getValue(false)*float64(card.SerraCount) + card.getValue(true)*float64(card.SerraCountFoil)
}
fmt.Printf("\nTotal Value: %s%.2f %s%s\n", Yellow, total, getCurrency(), Reset)
}
func show_card_details(card *Card) error {
fmt.Printf("%s%s%s (%s/%s)\n", Purple, card.Name, Reset, card.Set, card.CollectorNumber)
fmt.Printf("Added: %s\n", stringToTime(card.SerraCreated))
fmt.Printf("Count Normal: %dx\n", card.SerraCount)
if card.SerraCountFoil > 0 {
fmt.Printf("Count Foil: %dx\n", card.SerraCountFoil)
}
if card.SerraCountEtched > 0 {
fmt.Printf("Count Etched: %dx\n", card.SerraCountFoil)
}
fmt.Printf("Rarity: %s\n", card.Rarity)
fmt.Printf("Scryfall: %s\n", strings.Replace(card.ScryfallURI, "?utm_source=api", "", 1))
fmt.Printf("Current Value: %s%.2f %s%s\n", Yellow, card.getValue(false), getCurrency(), Reset)
if card.SerraCountFoil > 0 {
fmt.Printf("Foil Value: %s%.2f %s%s\n", Yellow, card.getValue(true), getCurrency(), Reset)
}
fmt.Printf("\n%sHistory%s\n", Green, Reset)
print_price_history(card.SerraPrices, "* ", false)
fmt.Println()
return nil
}

28
src/serra/env.go Normal file
View File

@ -0,0 +1,28 @@
package serra
import (
"log"
"os"
)
func getMongoDBURI() string {
uri := os.Getenv("MONGODB_URI")
if uri == "" {
log.Fatal("You must set your 'MONGODB_URI' environmental variable. See\n\t https://docs.mongodb.com/drivers/go/current/usage-examples/#environment-variable")
}
return uri
}
// Returns configured human readable name for
// the configured currency of the user
func getCurrency() string {
switch os.Getenv("SERRA_CURRENCY") {
case "EUR":
return "EUR"
case "USD":
return "USD"
}
// default
LogMessage("Warning: You did not configure SERRA_CURRENCY. Assuming \"USD\"", "yellow")
return "USD"
}

View File

@ -43,10 +43,10 @@ var flopsCmd = &cobra.Command{
func Gains(limit float64, sort int) error { func Gains(limit float64, sort int) error {
client := storageConnect() client := storage_connect()
coll := &Collection{client.Database("serra").Collection("cards")} coll := &Collection{client.Database("serra").Collection("cards")}
setcoll := &Collection{client.Database("serra").Collection("sets")} setcoll := &Collection{client.Database("serra").Collection("sets")}
defer storageDisconnect(client) defer storage_disconnect(client)
var old int var old int
if sinceBeginning { if sinceBeginning {
@ -57,11 +57,11 @@ func Gains(limit float64, sort int) error {
} }
currencyField := "$serra_prices.usd" currencyField := "$serra_prices.usd"
if getCurrency() == EUR { if getCurrency() == "EUR" {
currencyField = "$serra_prices.eur" currencyField = "$serra_prices.eur"
} }
raisePipeline := mongo.Pipeline{ raise_pipeline := mongo.Pipeline{
bson.D{{"$project", bson.D{{"$project",
bson.D{ bson.D{
{"name", true}, {"name", true},
@ -109,9 +109,9 @@ func Gains(limit float64, sort int) error {
bson.D{{"rate", sort}}}}, bson.D{{"rate", sort}}}},
bson.D{{"$limit", 20}}, bson.D{{"$limit", 20}},
} }
raise, _ := coll.storageAggregate(raisePipeline) raise, _ := coll.storage_aggregate(raise_pipeline)
sraisePipeline := mongo.Pipeline{ sraise_pipeline := mongo.Pipeline{
bson.D{{"$project", bson.D{{"$project",
bson.D{ bson.D{
{"name", true}, {"name", true},
@ -157,25 +157,25 @@ func Gains(limit float64, sort int) error {
bson.D{{"rate", sort}}}}, bson.D{{"rate", sort}}}},
bson.D{{"$limit", 10}}, bson.D{{"$limit", 10}},
} }
sraise, _ := setcoll.storageAggregate(sraisePipeline) sraise, _ := setcoll.storage_aggregate(sraise_pipeline)
// percentage coloring // percentage coloring
var pColor string var p_color string
if sort == 1 { if sort == 1 {
pColor = Red p_color = Red
} else { } else {
pColor = Green p_color = Green
} }
fmt.Printf("%sCards%s\n", Purple, Reset) fmt.Printf("%sCards%s\n", Purple, Reset)
// print each card // print each card
for _, e := range raise { for _, e := range raise {
fmt.Printf("%s%+.0f%%%s %s %s(%s/%s)%s (%.2f->%s%.2f%s%s) \n", pColor, e["rate"], Reset, e["name"], Yellow, e["set"], e["collectornumber"], Reset, e["old"], Green, e["current"], getCurrency(), Reset) fmt.Printf("%s%+.0f%%%s %s %s(%s/%s)%s (%.2f->%s%.2f %s%s) \n", p_color, e["rate"], Reset, e["name"], Yellow, e["set"], e["collectornumber"], Reset, e["old"], Green, e["current"], getCurrency(), Reset)
} }
fmt.Printf("\n%sSets%s\n", Purple, Reset) fmt.Printf("\n%sSets%s\n", Purple, Reset)
for _, e := range sraise { for _, e := range sraise {
fmt.Printf("%s%+.0f%%%s %s %s(%s)%s (%.2f->%s%.2f%s%s) \n", pColor, e["rate"], Reset, e["name"], Yellow, e["code"], Reset, e["old"], Green, e["current"], getCurrency(), Reset) fmt.Printf("%s%+.0f%%%s %s %s(%s)%s (%.2f->%s%.2f %s%s) \n", p_color, e["rate"], Reset, e["name"], Yellow, e["code"], Reset, e["old"], Green, e["current"], getCurrency(), Reset)
} }
return nil return nil

227
src/serra/helpers.go Normal file
View File

@ -0,0 +1,227 @@
package serra
import (
"errors"
"fmt"
"math"
"strconv"
"time"
"unicode"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type Rarities struct {
Rares, Uncommons, Commons, Mythics float64
}
func modify_count_of_card(coll *Collection, c *Card, amount int64, foil bool) error {
// find already existing card
sort := bson.D{{"_id", 1}}
search_filter := bson.D{{"_id", c.ID}}
stored_cards, err := coll.storage_find(search_filter, sort)
if err != nil {
return err
}
stored_card := stored_cards[0]
// update card amount
update_filter := bson.M{"_id": bson.M{"$eq": c.ID}}
var update bson.M
if foil {
update = bson.M{
"$set": bson.M{"serra_count_foil": stored_card.SerraCountFoil + amount},
}
} else {
update = bson.M{
"$set": bson.M{"serra_count": stored_card.SerraCount + amount},
}
}
coll.storage_update(update_filter, update)
var total int64
if foil {
total = stored_card.SerraCountFoil + amount
} else {
total = stored_card.SerraCount + amount
}
LogMessage(fmt.Sprintf("Updating Card \"%s\" amount to %d", stored_card.Name, total), "purple")
return nil
}
func find_card_by_setcollectornumber(coll *Collection, setcode string, collectornumber string) (*Card, error) {
sort := bson.D{{"_id", 1}}
search_filter := bson.D{{"set", setcode}, {"collectornumber", collectornumber}}
stored_cards, err := coll.storage_find(search_filter, sort)
if err != nil {
return &Card{}, err
}
if len(stored_cards) < 1 {
return &Card{}, errors.New("Card not found")
}
return &stored_cards[0], nil
}
func stringToTime(s primitive.DateTime) string {
return time.UnixMilli(int64(s)).Format("2006-01-02")
}
// missing compares two slices and returns slice of differences
func missing(a, b []string) []string {
type void struct{}
// create map with length of the 'a' slice
ma := make(map[string]void, len(a))
diffs := []string{}
// Convert first slice to map with empty struct (0 bytes)
for _, ka := range a {
ma[ka] = void{}
}
// find missing values in a
for _, kb := range b {
if _, ok := ma[kb]; !ok {
diffs = append(diffs, kb)
}
}
return diffs
}
func find_set_by_code(coll *Collection, setcode string) (*Set, error) {
stored_sets, err := coll.storage_find_set(bson.D{{"code", setcode}}, bson.D{{"_id", 1}})
if err != nil {
return &Set{}, err
}
if len(stored_sets) < 1 {
return &Set{}, errors.New("Set not found")
}
return &stored_sets[0], nil
}
func convert_mana_symbols(sym []interface{}) string {
var mana string
if len(sym) == 0 {
// mana = mana + "\U0001F6AB" //probibited sign for lands
mana = mana + "None" //probibited sign for lands
}
for _, v := range sym {
switch v {
case "B":
mana = mana + "Black" //black
//mana = mana + "\U000026AB" //black
case "R":
mana = mana + "Red" //red
// mana = mana + "\U0001F534" //red
case "G":
mana = mana + "Green" //green
// mana = mana + "\U0001F7E2" //green
case "U":
mana = mana + "Blue" //blue
//mana = mana + "\U0001F535" //blue
case "W":
mana = mana + "White" //white
// mana = mana + "\U000026AA" //white
}
}
return mana
}
func convert_rarities(rar []primitive.M) Rarities {
// this is maybe the ugliest way someone could choose to verify, if a rarity type is missing
// [
// { _id: { rarity: 'common' }, count: 20 },
// { _id: { rarity: 'uncommon' }, count: 2 }
// ]
// if a result like this is there, 1 rarity type "rare" is not in the array. and needs to be
// initialized with 0, otherwise we get a panic
var ri Rarities
for _, r := range rar {
switch r["_id"] {
case "rare":
ri.Rares = r["count"].(float64)
case "uncommon":
ri.Uncommons = r["count"].(float64)
case "common":
ri.Commons = r["count"].(float64)
case "mythic":
ri.Mythics = r["count"].(float64)
}
}
return ri
}
func print_price_history(prices []PriceEntry, prefix string, total bool) {
var before float64
for _, e := range prices {
var value float64
if total {
value = e.Usd + e.UsdFoil + e.UsdEtched
if getCurrency() == "EUR" {
value = e.Eur + e.EurFoil
}
} else {
value = e.Usd
if getCurrency() == "EUR" {
value = e.Eur
}
}
if value > before && before != 0 {
fmt.Printf("%s%s%s %.2f %s%s (%+.2f%%, %+.2f %s)\n", prefix, stringToTime(e.Date), Green, value, getCurrency(), Reset, (value/before*100)-100, value-before, getCurrency())
} else if value < before {
fmt.Printf("%s%s%s %.2f %s%s (%+.2f%%, %+.2f %s)\n", prefix, stringToTime(e.Date), Red, value, getCurrency(), Reset, (value/before*100)-100, value-before, getCurrency())
} else {
fmt.Printf("%s%s %.2f %s%s\n", prefix, stringToTime(e.Date), value, getCurrency(), Reset)
}
before = value
}
}
func filterForDigits(str string) int {
var numStr string
for _, c := range str {
if unicode.IsDigit(c) {
numStr += string(c)
}
}
s, _ := strconv.Atoi(numStr)
return s
}
func getFloat64(unknown interface{}) (float64, error) {
switch i := unknown.(type) {
case float64:
return i, nil
case float32:
return float64(i), nil
case int64:
return float64(i), nil
case int32:
return float64(i), nil
case int:
return float64(i), nil
case uint64:
return float64(i), nil
case uint32:
return float64(i), nil
case uint:
return float64(i), nil
default:
return math.NaN(), errors.New("Non-numeric type could not be converted to float")
}
}

65
src/serra/missing.go Normal file
View File

@ -0,0 +1,65 @@
package serra
import (
"fmt"
"strconv"
"github.com/spf13/cobra"
"go.mongodb.org/mongo-driver/bson"
)
func init() {
rootCmd.AddCommand(missingCmd)
}
var missingCmd = &cobra.Command{
Aliases: []string{"m"},
Use: "missing <set>",
Short: "Display missing cards from a set",
Long: `In case you are a set collector, you can generate a list of
cards you dont own (yet) :)`,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, setname []string) error {
client := storage_connect()
coll := &Collection{client.Database("serra").Collection("cards")}
defer storage_disconnect(client)
// fetch all cards in set
cards, err := coll.storage_find(bson.D{{"set", setname[0]}}, bson.D{{"collectornumber", 1}})
if (err != nil) || len(cards) == 0 {
LogMessage(fmt.Sprintf("Error: Set %s not found or no card in your collection.", setname[0]), "red")
return err
}
// fetch set informations
setcoll := &Collection{client.Database("serra").Collection("sets")}
sets, _ := setcoll.storage_find_set(bson.D{{"code", setname[0]}}, bson.D{{"_id", 1}})
set := sets[0]
LogMessage(fmt.Sprintf("Missing cards in %s", sets[0].Name), "green")
// generate set with all setnumbers
var complete_set []string
var i int64
for i = 1; i <= set.CardCount; i++ {
complete_set = append(complete_set, strconv.FormatInt(i, 10))
}
// iterate over all cards in collection
var in_collection []string
for _, c := range cards {
in_collection = append(in_collection, c.CollectorNumber)
}
misses := missing(in_collection, complete_set)
for _, m := range misses {
ncard, err := fetch_card(fmt.Sprintf("%s/%s", setname[0], m))
if err != nil {
continue
}
fmt.Printf("%.02f %s\t%s (%s)\n", ncard.getValue(false), getCurrency(), ncard.Name, ncard.SetName)
}
return nil
},
}

View File

@ -2,6 +2,7 @@ package serra
import ( import (
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/chzyer/readline" "github.com/chzyer/readline"
@ -26,7 +27,7 @@ var removeCmd = &cobra.Command{
RunE: func(cmd *cobra.Command, cards []string) error { RunE: func(cmd *cobra.Command, cards []string) error {
if interactive { if interactive {
removeCardsInteractive(set) removeCardsInteractive(unique, set)
} else { } else {
removeCards(cards, count) removeCards(cards, count)
} }
@ -34,11 +35,11 @@ var removeCmd = &cobra.Command{
}, },
} }
func removeCardsInteractive(set string) { func removeCardsInteractive(unique bool, set string) {
l := Logger()
if len(set) == 0 { if len(set) == 0 {
l.Fatal("Option --set must be given in interactive mode") LogMessage("Error: --set must be given in interactive mode", "red")
os.Exit(1)
} }
rl, err := readline.New(fmt.Sprintf("%s> ", set)) rl, err := readline.New(fmt.Sprintf("%s> ", set))
@ -63,57 +64,37 @@ func removeCardsInteractive(set string) {
} }
func removeCards(cards []string, count int64) error { func removeCards(cards []string, count int64) error {
// Connect to the DB & load the collection
client := storageConnect() client := storage_connect()
coll := &Collection{client.Database("serra").Collection("cards")} coll := &Collection{client.Database("serra").Collection("cards")}
l := Logger() defer storage_disconnect(client)
defer storageDisconnect(client)
// Loop over different cards // Loop over different cards
for _, card := range cards { for _, card := range cards {
if !strings.Contains(card, "/") {
l.Errorf("Invalid card format %s. Needs to be set/collector number i.e. \"usg/13\"", card)
continue
}
// Extract collector number and set name from input & remove leading zeros
collectorNumber := strings.TrimLeft(strings.Split(card, "/")[1], "0")
setName := strings.Split(card, "/")[0]
if collectorNumber == "" {
l.Errorf("Invalid card format %s. Needs to be set/collector number i.e. \"usg/13\"", card)
continue
}
// Fetch card from scryfall // Fetch card from scryfall
c, err := findCardByCollectorNumber(coll, setName, collectorNumber) c, err := find_card_by_setcollectornumber(coll, strings.Split(card, "/")[0], strings.Split(card, "/")[1])
if err != nil { if err != nil {
l.Error(err) LogMessage(fmt.Sprintf("%v", err), "red")
continue continue
} }
if foil && c.SerraCountFoil < 1 { if foil && c.SerraCountFoil < 1 {
l.Errorf("No foil \"%s\" in the collection", c.Name) LogMessage(fmt.Sprintf("Error: No Foil \"%s\" in the Collection.", c.Name), "red")
continue continue
} }
if !foil && c.SerraCount < 1 { if !foil && c.SerraCount < 1 {
l.Errorf("No normal \"%s\" in the collection", c.Name) LogMessage(fmt.Sprintf("Error: No Non-Foil \"%s\" in the Collection.", c.Name), "red")
continue
}
if !askConfirmation(c) {
continue continue
} }
if foil && c.SerraCountFoil == 1 && c.SerraCount == 0 || !foil && c.SerraCount == 1 && c.SerraCountFoil == 0 { if foil && c.SerraCountFoil == 1 && c.SerraCount == 0 || !foil && c.SerraCount == 1 && c.SerraCountFoil == 0 {
coll.storageRemove(bson.M{"_id": c.ID}) coll.storage_remove(bson.M{"_id": c.ID})
l.Infof("\"%s\" (%.2f%s) removed", c.Name, c.getValue(foil), getCurrency()) LogMessage(fmt.Sprintf("\"%s\" (%.2f %s) removed from the Collection.", c.Name, c.getValue(foil), getCurrency()), "green")
} else { } else {
modifyCardCount(coll, c, -count, foil) modify_count_of_card(coll, c, -1, foil)
}
} }
}
return nil return nil
} }

39
src/serra/root.go Normal file
View File

@ -0,0 +1,39 @@
package serra
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var Version = "unknown"
var count int64
var limit float64
var interactive bool
var name string
var oracle string
var rarity string
var set string
var sinceBeginning bool
var sinceLastUpdate bool
var sortby string
var cardType string
var unique bool
var foil bool
var rootCmd = &cobra.Command{
Version: Version,
Long: `serra - Magic: The Gathering Collection Tracker`,
Use: "serra",
DisableFlagsInUseLine: true,
SilenceErrors: true,
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

View File

@ -2,14 +2,13 @@ package serra
import ( import (
"bytes" "bytes"
"encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
"os" "strings"
"time" "time"
"go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/bson/primitive"
@ -20,21 +19,16 @@ type Card struct {
SerraCount int64 `bson:"serra_count"` SerraCount int64 `bson:"serra_count"`
SerraCountFoil int64 `bson:"serra_count_foil"` SerraCountFoil int64 `bson:"serra_count_foil"`
SerraCountEtched int64 `bson:"serra_count_etched"` SerraCountEtched int64 `bson:"serra_count_etched"`
SerraCountDeck int64 `bson:"serra_count_deck"`
SerraCountFoilDeck int64 `bson:"serra_count_foil_deck"`
SerraCountEtchedDeck int64 `bson:"serra_count_etched_deck"`
SerraPrices []PriceEntry `bson:"serra_prices"` SerraPrices []PriceEntry `bson:"serra_prices"`
SerraCreated primitive.DateTime `bson:"serra_created"` SerraCreated primitive.DateTime `bson:"serra_created"`
SerraUpdated primitive.DateTime `bson:"serra_updated"` SerraUpdated primitive.DateTime `bson:"serra_updated"`
SerraImage64 string `bson:"serra_image"`
Artist string `json:"artist"` Artist string `json:"artist"`
ArtistIds []string `json:"artist_ids"` ArtistIds []string `json:"artist_ids"`
Booster bool `json:"booster"` Booster bool `json:"booster"`
BorderColor string `json:"border_color"` BorderColor string `json:"border_color"`
CardBackID string `json:"card_back_id"` CardBackID string `json:"card_back_id"`
CardmarketID float64 `json:"cardmarket_id"` Cmc int64 `json:"cmc"`
Cmc float64 `json:"cmc"`
CollectorNumber string `json:"collector_number"` CollectorNumber string `json:"collector_number"`
ColorIdentity []string `json:"color_identity"` ColorIdentity []string `json:"color_identity"`
Colors []string `json:"colors"` Colors []string `json:"colors"`
@ -120,125 +114,23 @@ type Card struct {
SetURI string `json:"set_uri"` SetURI string `json:"set_uri"`
StorySpotlight bool `json:"story_spotlight"` StorySpotlight bool `json:"story_spotlight"`
Textless bool `json:"textless"` Textless bool `json:"textless"`
TCGPlayerID float64 `json:"tcgplayer_id"`
TypeLine string `json:"type_line"` TypeLine string `json:"type_line"`
URI string `json:"uri"` URI string `json:"uri"`
Variation bool `json:"variation"` Variation bool `json:"variation"`
} }
type BulkIndex struct { // Getter for currency specific value
Object string `json:"object"` func (c Card) getValue(foil bool) float64 {
HasMore bool `json:"has_more"` if getCurrency() == "EUR" {
Data []struct { if foil {
Object string `json:"object"` return c.Prices.EurFoil
ID string `json:"id"`
Type string `json:"type"`
UpdatedAt time.Time `json:"updated_at"`
URI string `json:"uri"`
Name string `json:"name"`
Description string `json:"description"`
Size int `json:"size"`
DownloadURI string `json:"download_uri"`
ContentType string `json:"content_type"`
ContentEncoding string `json:"content_encoding"`
} `json:"data"`
}
func fetchBulkDownloadURL() (string, error) {
url := "https://api.scryfall.com/bulk-data"
downloadURL := ""
// Make an HTTP GET request
resp, err := http.Get(url)
if err != nil {
log.Fatalf("Error fetching data: %v", err)
} }
defer resp.Body.Close() return c.Prices.Eur
// Read the response body
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("Error reading response body: %v", err)
} }
if foil {
// Unmarshal the JSON response return c.Prices.UsdFoil
var bulkData BulkIndex
if err := json.Unmarshal(body, &bulkData); err != nil {
log.Fatalf("Error unmarshaling JSON: %v", err)
} }
return c.Prices.Usd
// Find and print the unique cards URL
for _, item := range bulkData.Data {
if item.Type == "default_cards" {
downloadURL = item.DownloadURI
}
}
return downloadURL, nil
}
func downloadBulkData(downloadURL string) (string, error) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "download")
if err != nil {
log.Fatalf("Error creating temporary directory: %v", err)
}
// defer os.RemoveAll(tempDir) // Clean up the directory when done
// Create a temporary file in the temporary directory
tempFile, err := os.CreateTemp(tempDir, "downloaded-*.json") // Adjust the extension if necessary
if err != nil {
log.Fatalf("Error creating temporary file: %v", err)
}
// defer tempFile.Close() // Ensure we close the file when we're done
// Download the file
resp, err := http.Get(downloadURL)
if err != nil {
log.Fatalf("Error downloading file: %v", err)
}
defer resp.Body.Close() // Make sure to close the response body
// Check for a successful response
if resp.StatusCode != http.StatusOK {
log.Fatalf("Error: received status code %d", resp.StatusCode)
}
// Copy the response body to the temporary file
_, err = io.Copy(tempFile, resp.Body)
if err != nil {
log.Fatalf("Error saving file: %v", err)
}
return tempFile.Name(), nil
}
func loadBulkFile(bulkFilePath string) ([]Card, error) {
var cards []Card
fileBytes, _ := os.ReadFile(bulkFilePath)
defer os.Remove(bulkFilePath)
err := json.Unmarshal(fileBytes, &cards)
if err != nil {
fmt.Println("Error unmarshalling bulk file:", err)
return cards, nil
}
return cards, nil
}
func getCardFromBulk(cards []Card, setName, collectorNumber string) (*Card, error) {
var foundCard Card
for _, v := range cards {
if v.CollectorNumber == collectorNumber && v.Set == setName {
foundCard = v
return &foundCard, nil
}
}
return &Card{}, fmt.Errorf("Card %s/%s not found in bulk data", setName, collectorNumber)
} }
type PriceEntry struct { type PriceEntry struct {
@ -278,35 +170,29 @@ type Set struct {
URI string `json:"uri"` URI string `json:"uri"`
} }
// Getter for currency specific value func fetch_card(path string) (*Card, error) {
func (c Card) getValue(foil bool) float64 {
if getCurrency() == EUR {
if foil {
return c.Prices.EurFoil
}
return c.Prices.Eur
}
if foil {
return c.Prices.UsdFoil
}
return c.Prices.Usd
}
func fetchCard(setName, collectorNumber string) (*Card, error) { if !strings.Contains(path, "/") {
resp, err := http.Get(fmt.Sprintf("https://api.scryfall.com/cards/%s/%s/", setName, collectorNumber)) err := errors.New(fmt.Sprintf("Card must follow format <set>/<number>, for example: ath/15"))
return &Card{}, err
}
// TODO better URL Building...
resp, err := http.Get(fmt.Sprintf("https://api.scryfall.com/cards/%s/", path))
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
return &Card{}, err return &Card{}, err
} }
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
return &Card{}, fmt.Errorf("Card %s/%s not found", setName, collectorNumber) err := errors.New(fmt.Sprintf("Error: %s not found", path))
return &Card{}, err
} }
//we read the response body on the line below. //We Read the response body on the line below.
body, err := io.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
log.Fatalf("%s", err) log.Fatalln(err)
return &Card{}, err return &Card{}, err
} }
@ -314,9 +200,6 @@ func fetchCard(setName, collectorNumber string) (*Card, error) {
decoder := json.NewDecoder(r) decoder := json.NewDecoder(r)
val := &Card{} val := &Card{}
err = decoder.Decode(val) err = decoder.Decode(val)
if err != nil {
log.Fatalf("%s", err)
}
// Set created Time // Set created Time
val.SerraCreated = primitive.NewDateTimeFromTime(time.Now()) val.SerraCreated = primitive.NewDateTimeFromTime(time.Now())
@ -325,36 +208,24 @@ func fetchCard(setName, collectorNumber string) (*Card, error) {
val.Prices.Date = primitive.NewDateTimeFromTime(time.Now()) val.Prices.Date = primitive.NewDateTimeFromTime(time.Now())
val.SerraPrices = append(val.SerraPrices, val.Prices) val.SerraPrices = append(val.SerraPrices, val.Prices)
imgResp, imgErr := http.Get(val.ImageUris.Png)
if imgErr != nil {
log.Fatalln(err)
return &Card{}, err
}
bytes, readErr := io.ReadAll(imgResp.Body)
if readErr != nil {
log.Fatalln(err)
return &Card{}, err
}
val.SerraImage64 = base64.StdEncoding.EncodeToString(bytes)
return val, nil return val, nil
} }
func fetchSets() (*SetList, error) { func fetch_sets() (*SetList, error) {
// TODO: better URL Building... // TODO better URL Building...
resp, err := http.Get("https://api.scryfall.com/sets") resp, err := http.Get(fmt.Sprintf("https://api.scryfall.com/sets"))
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
return &SetList{}, err return &SetList{}, err
} }
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
return &SetList{}, fmt.Errorf("/sets not found") err := errors.New(fmt.Sprintf("Error: /sets not found"))
return &SetList{}, err
} }
//We Read the response body on the line below. //We Read the response body on the line below.
body, err := io.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
return &SetList{}, err return &SetList{}, err
@ -363,11 +234,7 @@ func fetchSets() (*SetList, error) {
r := bytes.NewReader(body) r := bytes.NewReader(body)
decoder := json.NewDecoder(r) decoder := json.NewDecoder(r)
val := &SetList{} val := &SetList{}
err = decoder.Decode(val) err = decoder.Decode(val)
if err != nil {
log.Fatalln(err)
}
return val, nil return val, nil
} }

View File

@ -25,7 +25,7 @@ otherwise you'll get a list of sets as a search result.`,
RunE: func(cmd *cobra.Command, set []string) error { RunE: func(cmd *cobra.Command, set []string) error {
if len(set) == 0 { if len(set) == 0 {
setList := Sets(sortby) setList := Sets(sortby)
showSetList(setList) show_set_list(setList)
} else { } else {
ShowSet(set[0]) ShowSet(set[0])
} }
@ -35,9 +35,9 @@ otherwise you'll get a list of sets as a search result.`,
func Sets(sort string) []primitive.M { func Sets(sort string) []primitive.M {
client := storageConnect() client := storage_connect()
coll := &Collection{client.Database("serra").Collection("cards")} coll := &Collection{client.Database("serra").Collection("cards")}
defer storageDisconnect(client) defer storage_disconnect(client)
groupStage := bson.D{ groupStage := bson.D{
{"$group", bson.D{ {"$group", bson.D{
@ -65,46 +65,45 @@ func Sets(sort string) []primitive.M {
}}} }}}
} }
sets, _ := coll.storageAggregate(mongo.Pipeline{groupStage, sortStage}) sets, _ := coll.storage_aggregate(mongo.Pipeline{groupStage, sortStage})
return sets return sets
} }
func showSetList(sets []primitive.M) { func show_set_list(sets []primitive.M) {
client := storageConnect() client := storage_connect()
setscoll := &Collection{client.Database("serra").Collection("sets")} setscoll := &Collection{client.Database("serra").Collection("sets")}
for _, set := range sets { for _, set := range sets {
setobj, _ := findSetByCode(setscoll, set["code"].(string)) setobj, _ := find_set_by_code(setscoll, set["code"].(string))
fmt.Printf("* %s %s%s%s (%s%s%s)\n", set["release"].(string)[0:4], Purple, set["_id"], Reset, Cyan, set["code"], Reset) fmt.Printf("* %s %s%s%s (%s%s%s)\n", set["release"].(string)[0:4], Purple, set["_id"], Reset, Cyan, set["code"], Reset)
fmt.Printf(" Cards: %s%d/%d%s Total: %.0f \n", Yellow, set["unique"], setobj.CardCount, Reset, set["count"]) fmt.Printf(" Cards: %s%d/%d%s Total: %.0f \n", Yellow, set["unique"], setobj.CardCount, Reset, set["count"])
fmt.Printf(" Value: %s%.2f%s%s\n", Pink, set["value"], getCurrency(), Reset) fmt.Printf(" Value: %s%.2f %s%s\n", Pink, set["value"], getCurrency(), Reset)
fmt.Println() fmt.Println()
} }
} }
func ShowSet(setname string) error { func ShowSet(setname string) error {
client := storageConnect() client := storage_connect()
coll := &Collection{client.Database("serra").Collection("cards")} coll := &Collection{client.Database("serra").Collection("cards")}
l := Logger() defer storage_disconnect(client)
defer storageDisconnect(client)
// fetch all cards in set ordered by currently used currency // fetch all cards in set ordered by currently used currency
cardSortCurrency := bson.D{{"prices.usd", -1}} cardSortCurrency := bson.D{{"prices.usd", -1}}
if getCurrency() == EUR { if getCurrency() == "EUR" {
cardSortCurrency = bson.D{{"prices.eur", -1}} cardSortCurrency = bson.D{{"prices.eur", -1}}
} }
cards, err := coll.storageFind(bson.D{{"set", setname}}, cardSortCurrency, 0, 0) cards, err := coll.storage_find(bson.D{{"set", setname}}, cardSortCurrency)
if (err != nil) || len(cards) == 0 { if (err != nil) || len(cards) == 0 {
l.Errorf("Set %s not found or no card in your collection.", setname) LogMessage(fmt.Sprintf("Error: Set %s not found or no card in your collection.", setname), "red")
return err return err
} }
// fetch set informations // fetch set informations
setcoll := &Collection{client.Database("serra").Collection("sets")} setcoll := &Collection{client.Database("serra").Collection("sets")}
sets, _ := setcoll.storageFindSet(bson.D{{"code", setname}}, bson.D{{"_id", 1}}) sets, _ := setcoll.storage_find_set(bson.D{{"code", setname}}, bson.D{{"_id", 1}})
// set values // set values
matchStage := bson.D{ matchStage := bson.D{
@ -118,10 +117,9 @@ func ShowSet(setname string) error {
{"value", bson.D{{"$sum", bson.D{{"$multiply", bson.A{getCurrencyField(false), "$serra_count"}}}}}}, {"value", bson.D{{"$sum", bson.D{{"$multiply", bson.A{getCurrencyField(false), "$serra_count"}}}}}},
{"value_foil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{getCurrencyField(true), "$serra_count_foil"}}}}}}, {"value_foil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{getCurrencyField(true), "$serra_count_foil"}}}}}},
{"count", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count"}}}}}}, {"count", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count"}}}}}},
{"count_foil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count_foil"}}}}}},
}}, }},
} }
stats, _ := coll.storageAggregate(mongo.Pipeline{matchStage, groupStage}) stats, _ := coll.storage_aggregate(mongo.Pipeline{matchStage, groupStage})
// set rarities // set rarities
matchStage = bson.D{ matchStage = bson.D{
@ -139,44 +137,32 @@ func ShowSet(setname string) error {
{"$sort", bson.D{ {"$sort", bson.D{
{"_id", 1}, {"_id", 1},
}}} }}}
rar, _ := coll.storageAggregate(mongo.Pipeline{matchStage, groupStage, sortStage}) rar, _ := coll.storage_aggregate(mongo.Pipeline{matchStage, groupStage, sortStage})
ri := convertRarities(rar) ri := convert_rarities(rar)
fmt.Printf("%s%s%s\n", Green, sets[0].Name, Reset) LogMessage(fmt.Sprintf("%s", sets[0].Name), "green")
fmt.Printf("Released: %s\n", sets[0].ReleasedAt) LogMessage(fmt.Sprintf("Set Cards: %d/%d", len(cards), sets[0].CardCount), "normal")
fmt.Printf("Set Cards: %d/%d\n", len(cards), sets[0].CardCount) LogMessage(fmt.Sprintf("Total Cards: %.0f", stats[0]["count"]), "normal")
fmt.Printf("Total Cards: %.0f\n", stats[0]["count"]) nf_value, err := getFloat64(stats[0]["value"])
fmt.Printf("Foil Cards: %.0f\n", stats[0]["count_foil"])
normalValue, err := getFloat64(stats[0]["value"])
if err != nil { if err != nil {
l.Error(err) LogMessage(fmt.Sprintf("Error: %v", err), "red")
normalValue = 0 nf_value = 0
} }
foilValue, err := getFloat64(stats[0]["value_foil"]) foil_value, err := getFloat64(stats[0]["value_foil"])
if err != nil { if err != nil {
l.Error(err) LogMessage(fmt.Sprintf("Error: %v", err), "red")
foilValue = 0 foil_value = 0
} }
totalValue := normalValue + foilValue total_value := nf_value + foil_value
LogMessage(fmt.Sprintf("Total Value: %.2f %s", total_value, getCurrency()), "normal")
normalCount, _ := getFloat64(stats[0]["count"]) LogMessage(fmt.Sprintf("Released: %s", sets[0].ReleasedAt), "normal")
foilCount, _ := getFloat64(stats[0]["count_foil"]) LogMessage(fmt.Sprintf("Mythics: %.0f", ri.Mythics), "normal")
LogMessage(fmt.Sprintf("Rares: %.0f", ri.Rares), "normal")
fmt.Printf("\n%sCurrent Value%s\n", Purple, Reset) LogMessage(fmt.Sprintf("Uncommons: %.0f", ri.Uncommons), "normal")
fmt.Printf("Total: %.0fx %s%.2f%s%s\n", normalCount+foilCount, Yellow, totalValue, getCurrency(), Reset) LogMessage(fmt.Sprintf("Commons: %.0f", ri.Commons), "normal")
fmt.Printf("Normal: %.0fx %s%.2f%s%s\n", stats[0]["count"], Yellow, normalValue, getCurrency(), Reset)
fmt.Printf("Foil: %.0fx %s%.2f%s%s\n", stats[0]["count_foil"], Yellow, foilValue, getCurrency(), Reset)
fmt.Printf("\n%sRarities%s\n", Purple, Reset)
fmt.Printf("Mythics: %.0f\n", ri.Mythics)
fmt.Printf("Rares: %.0f\n", ri.Rares)
fmt.Printf("Uncommons: %.0f\n", ri.Uncommons)
fmt.Printf("Commons: %.0f\n", ri.Commons)
fmt.Printf("\n%sPrice History:%s\n", Pink, Reset) fmt.Printf("\n%sPrice History:%s\n", Pink, Reset)
showPriceHistory(sets[0].SerraPrices, "* ", true) print_price_history(sets[0].SerraPrices, "* ", true)
fmt.Printf("\n%sMost valuable cards%s\n", Pink, Reset) fmt.Printf("\n%sMost valuable cards%s\n", Pink, Reset)
@ -190,7 +176,8 @@ func ShowSet(setname string) error {
for i := 0; i < ccards; i++ { for i := 0; i < ccards; i++ {
card := cards[i] card := cards[i]
fmt.Printf("* %s%s%s (%s/%s) %s%.2f%s%s\n", Purple, card.Name, Reset, sets[0].Code, card.CollectorNumber, Yellow, card.getValue(false), getCurrency(), Reset) fmt.Printf("* %dx %s%s%s (%s/%s) %s%.2f %s%s\n", card.SerraCount, Purple, card.Name, Reset, sets[0].Code, card.CollectorNumber, Yellow, card.getValue(false), getCurrency(), Reset)
fmt.Printf("* %dx %s%s%s (%s/%s) %s%.2f %s%s\n", card.SerraCountFoil, Purple, card.Name, Reset, sets[0].Code, card.CollectorNumber, Yellow, card.getValue(true), getCurrency(), Reset)
} }
return nil return nil

132
src/serra/stats.go Normal file
View File

@ -0,0 +1,132 @@
package serra
import (
"fmt"
"github.com/spf13/cobra"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
func init() {
rootCmd.AddCommand(statsCmd)
}
var statsCmd = &cobra.Command{
Aliases: []string{"stats"},
Use: "stats <prefix> <n>",
Short: "Shows statistics of the collection",
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
client := storage_connect()
coll := &Collection{client.Database("serra").Collection("cards")}
totalcoll := &Collection{client.Database("serra").Collection("total")}
defer storage_disconnect(client)
sets, _ := coll.storage_aggregate(mongo.Pipeline{
bson.D{
{"$match", bson.D{
{"coloridentity", bson.D{{"$size", 1}}}}}},
bson.D{
{"$group", bson.D{
{"_id", "$coloridentity"},
{"count", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count"}}}}}},
}}},
bson.D{
{"$sort", bson.D{
{"count", -1},
}}},
})
fmt.Printf("%sColors%s\n", Green, Reset)
for _, set := range sets {
x, _ := set["_id"].(primitive.A)
s := []interface{}(x)
fmt.Printf("%s: %s%.0f%s\n", convert_mana_symbols(s), Purple, set["count"], Reset)
}
stats, _ := coll.storage_aggregate(mongo.Pipeline{
bson.D{
{"$group", bson.D{
{"_id", nil},
{"value", bson.D{{"$sum", bson.D{{"$multiply", bson.A{getCurrencyField(false), "$serra_count"}}}}}},
{"value_foil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{getCurrencyField(true), "$serra_count_foil"}}}}}},
{"count", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count"}}}}}},
{"count_foil", bson.D{{"$sum", "$serra_count_foil"}}},
{"count_etched", bson.D{{"$sum", "$serra_count_etched"}}},
{"rarity", bson.D{{"$sum", "$rarity"}}},
{"unique", bson.D{{"$sum", 1}}},
}},
},
bson.D{
{"$addFields", bson.D{
{"count_all", bson.D{{"$sum", bson.A{"$count", "$count_foil", "$count_etched"}}}},
}},
},
})
fmt.Printf("\n%sCards %s\n", Green, Reset)
fmt.Printf("Total: %s%.0f%s\n", Yellow, stats[0]["count_all"], Reset)
fmt.Printf("Unique: %s%d%s\n", Purple, stats[0]["unique"], Reset)
fmt.Printf("Normal: %s%.0f%s\n", Purple, stats[0]["count"], Reset)
fmt.Printf("Foil: %s%d%s\n", Purple, stats[0]["count_foil"], Reset)
fmt.Printf("Etched: %s%d%s\n", Purple, stats[0]["count_etched"], Reset)
reserved, _ := coll.storage_aggregate(mongo.Pipeline{
bson.D{
{"$match", bson.D{
{"reserved", true}}}},
bson.D{
{"$group", bson.D{
{"_id", nil},
{"count", bson.D{{"$sum", 1}}},
}}},
})
var count_reserved float64
if len(reserved) > 0 {
count_reserved = reserved[0]["count"].(float64)
}
fmt.Printf("Reserved List: %s%.0f%s\n", Yellow, count_reserved, Reset)
rar, _ := coll.storage_aggregate(mongo.Pipeline{
bson.D{
{"$group", bson.D{
{"_id", "$rarity"},
{"count", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count"}}}}}},
}}},
bson.D{
{"$sort", bson.D{
{"_id", 1},
}}},
})
ri := convert_rarities(rar)
fmt.Printf("\n%sRarity%s\n", Green, Reset)
fmt.Printf("Mythics: %s%.0f%s\n", Pink, ri.Mythics, Reset)
fmt.Printf("Rares: %s%.0f%s\n", Pink, ri.Rares, Reset)
fmt.Printf("Uncommons: %s%.0f%s\n", Yellow, ri.Uncommons, Reset)
fmt.Printf("Commons: %s%.0f%s\n", Purple, ri.Commons, Reset)
fmt.Printf("\n%sTotal Value%s\n", Green, Reset)
nf_value, err := getFloat64(stats[0]["value"])
if err != nil {
LogMessage(fmt.Sprintf("Error: %v", err), "red")
nf_value = 0
}
foil_value, err := getFloat64(stats[0]["value_foil"])
if err != nil {
LogMessage(fmt.Sprintf("Error: %v", err), "red")
foil_value = 0
}
total_value := nf_value + foil_value
fmt.Printf("Total: %s%.2f %s%s\n", Pink, total_value, getCurrency(), Reset)
fmt.Printf("Normal: %s%.2f %s%s\n", Pink, nf_value, getCurrency(), Reset)
fmt.Printf("Foils: %s%.2f %s%s\n", Pink, foil_value, getCurrency(), Reset)
total, _ := totalcoll.storage_find_total()
fmt.Printf("History: \n")
print_price_history(total.Value, "* ", true)
return nil
},
}

View File

@ -3,6 +3,7 @@ package serra
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"os" "os"
"time" "time"
@ -17,8 +18,7 @@ type Total struct {
Value []PriceEntry `bson:"value"` Value []PriceEntry `bson:"value"`
} }
// Collection Struct // https://siongui.github.io/2017/02/11/go-add-method-function-to-type-in-external-package/
// reason: https://siongui.github.io/2017/02/11/go-add-method-function-to-type-in-external-package/
type Collection struct { type Collection struct {
*mongo.Collection *mongo.Collection
} }
@ -45,20 +45,20 @@ func getCurrencyField(foil bool) string {
} }
} }
func storageConnect() *mongo.Client { func storage_connect() *mongo.Client {
l := Logger()
uri := getMongoDBURI()
uri := getMongoDBURI()
client, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(uri)) client, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(uri))
if err != nil { if err != nil {
l.Fatalf("Could not connect to mongodb at %s", uri) LogMessage(fmt.Sprintf("Could not connect to mongodb at %s", uri), "red")
os.Exit(1)
} }
return client return client
} }
func (coll Collection) storageAdd(card *Card) error { func (coll Collection) storage_add(card *Card) error {
card.SerraUpdated = primitive.NewDateTimeFromTime(time.Now()) card.SerraUpdated = primitive.NewDateTimeFromTime(time.Now())
@ -70,7 +70,7 @@ func (coll Collection) storageAdd(card *Card) error {
} }
func (coll Collection) storageAddSet(set *Set) (*mongo.InsertOneResult, error) { func (coll Collection) storage_add_set(set *Set) (*mongo.InsertOneResult, error) {
id, err := coll.InsertOne(context.TODO(), set) id, err := coll.InsertOne(context.TODO(), set)
if err != nil { if err != nil {
@ -80,7 +80,7 @@ func (coll Collection) storageAddSet(set *Set) (*mongo.InsertOneResult, error) {
} }
func (coll Collection) storageAddTotal(p PriceEntry) error { func (coll Collection) storage_add_total(p PriceEntry) error {
// create total object if not exists... // create total object if not exists...
coll.InsertOne(context.TODO(), Total{ID: "1", Value: []PriceEntry{}}) coll.InsertOne(context.TODO(), Total{ID: "1", Value: []PriceEntry{}})
@ -101,89 +101,91 @@ func (coll Collection) storageAddTotal(p PriceEntry) error {
return nil return nil
} }
func (coll Collection) storageFind(filter, sort bson.D, skip, limit int64) ([]Card, error) { func (coll Collection) storage_find(filter, sort bson.D) ([]Card, error) {
opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit)
cursor, err := coll.Find(context.TODO(), filter, opts)
l := Logger()
opts := options.Find().SetSort(sort)
cursor, err := coll.Find(context.TODO(), filter, opts)
if err != nil { if err != nil {
l.Fatalf("Could not query data due to connection errors to database: %s", err.Error()) LogMessage("Could not query data due to connection errors to database", "red")
os.Exit(1)
} }
var results []Card var results []Card
if err = cursor.All(context.TODO(), &results); err != nil { if err = cursor.All(context.TODO(), &results); err != nil {
l.Fatal(err) log.Fatal(err)
return []Card{}, err return []Card{}, err
} }
return results, nil return results, nil
} }
func (coll Collection) storageFindSet(filter, sort bson.D) ([]Set, error) { func (coll Collection) storage_find_set(filter, sort bson.D) ([]Set, error) {
l := Logger()
opts := options.Find().SetSort(sort)
opts := options.Find().SetSort(sort)
cursor, err := coll.Find(context.TODO(), filter, opts) cursor, err := coll.Find(context.TODO(), filter, opts)
if err != nil { if err != nil {
l.Fatalf("Could not query set data due to connection errors to database: %s", err.Error()) LogMessage("Could not query set data due to connection errors to database", "red")
os.Exit(1)
} }
var results []Set var results []Set
if err = cursor.All(context.TODO(), &results); err != nil { if err = cursor.All(context.TODO(), &results); err != nil {
l.Fatal(err) log.Fatal(err)
return []Set{}, err return []Set{}, err
} }
return results, nil return results, nil
} }
func (coll Collection) storageFindTotal() (Total, error) { func (coll Collection) storage_find_total() (Total, error) {
var total Total var total Total
l := Logger()
err := coll.FindOne(context.TODO(), bson.D{{"_id", "1"}}).Decode(&total) err := coll.FindOne(context.TODO(), bson.D{{"_id", "1"}}).Decode(&total)
if err != nil {
l.Fatalf("Could not query total data due to connection errors to database: %s", err.Error())
}
if err != nil {
LogMessage("Could not query total data due to connection errors to database", "red")
os.Exit(1)
}
return total, nil return total, nil
} }
func (coll Collection) storageRemove(filter bson.M) error { func (coll Collection) storage_remove(filter bson.M) error {
l := Logger()
_, err := coll.DeleteOne(context.TODO(), filter) _, err := coll.DeleteOne(context.TODO(), filter)
if err != nil { if err != nil {
l.Fatalf("Could remove card data due to connection errors to database: %s", err.Error()) LogMessage("Could remove card data due to connection errors to database", "red")
os.Exit(1)
} }
return nil return nil
} }
func (coll Collection) storageAggregate(pipeline mongo.Pipeline) ([]primitive.M, error) { func (coll Collection) storage_aggregate(pipeline mongo.Pipeline) ([]primitive.M, error) {
l := Logger()
opts := options.Aggregate()
opts := options.Aggregate()
cursor, err := coll.Aggregate( cursor, err := coll.Aggregate(
context.TODO(), context.TODO(),
pipeline, pipeline,
opts) opts)
if err != nil { if err != nil {
l.Fatalf("Could not aggregate data due to connection errors to database: %s", err.Error()) LogMessage(fmt.Sprintf("%v", err), "red")
LogMessage("Could not aggregate data due to connection errors to database", "red")
os.Exit(1)
} }
// Get a list of all returned documents and print them out. // Get a list of all returned documents and print them out.
// See the mongo.Cursor documentation for more examples of using cursors. // See the mongo.Cursor documentation for more examples of using cursors.
var results []bson.M var results []bson.M
if err = cursor.All(context.TODO(), &results); err != nil { if err = cursor.All(context.TODO(), &results); err != nil {
l.Fatal(err) log.Fatal(err)
} }
return results, nil return results, nil
} }
func (coll Collection) storageUpdate(filter, update bson.M) error { func (coll Collection) storage_update(filter, update bson.M) error {
l := Logger()
// Call the driver's UpdateOne() method and pass filter and update to it // Call the driver's UpdateOne() method and pass filter and update to it
_, err := coll.UpdateOne( _, err := coll.UpdateOne(
context.Background(), context.Background(),
@ -191,13 +193,14 @@ func (coll Collection) storageUpdate(filter, update bson.M) error {
update, update,
) )
if err != nil { if err != nil {
l.Fatalf("Could not update data due to connection errors to database: %s", err.Error()) LogMessage("Could not update data due to connection errors to database", "red")
os.Exit(1)
} }
return nil return nil
} }
func storageDisconnect(client *mongo.Client) error { func storage_disconnect(client *mongo.Client) error {
if err := client.Disconnect(context.TODO()); err != nil { if err := client.Disconnect(context.TODO()); err != nil {
return err return err
} }

View File

@ -19,25 +19,24 @@ func init() {
var updateCmd = &cobra.Command{ var updateCmd = &cobra.Command{
Aliases: []string{"u"}, Aliases: []string{"u"},
Use: "update", Use: "update",
Short: "update card values from scryfall", Short: "Update card values from scryfall",
Long: `the update mechanism iterates over each card in your collection and fetches its price. after all cards you own in a set are updated, the set value will update. after all sets are updated, the whole collection value is updated.`, Long: `The update mechanism iterates over each card in your collection and fetches its price. After all cards you own in a set are updated, the set value will update. After all Sets are updated, the whole collection value is updated.`,
SilenceErrors: true, SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
client := storageConnect() client := storage_connect()
l := Logger() defer storage_disconnect(client)
defer storageDisconnect(client)
// update sets // update sets
setscoll := &Collection{client.Database("serra").Collection("sets")} setscoll := &Collection{client.Database("serra").Collection("sets")}
coll := &Collection{client.Database("serra").Collection("cards")} coll := &Collection{client.Database("serra").Collection("cards")}
totalcoll := &Collection{client.Database("serra").Collection("total")} totalcoll := &Collection{client.Database("serra").Collection("total")}
// predefine query for set analysis. used for total stats later
projectStage := bson.D{{"$project", projectStage := bson.D{{"$project",
bson.D{ bson.D{
{"serra_count", true}, {"serra_count", true},
{"serra_count_foil", true}, {"serra_count_foil", true},
{"serra_count_etched", true},
{"set", true}, {"set", true},
{"last_price", bson.D{{"$arrayElemAt", bson.A{"$serra_prices", -1}}}}}}} {"last_price", bson.D{{"$arrayElemAt", bson.A{"$serra_prices", -1}}}}}}}
groupStage := bson.D{ groupStage := bson.D{
@ -46,42 +45,19 @@ var updateCmd = &cobra.Command{
{"eur", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.eur", "$serra_count"}}}}}}, {"eur", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.eur", "$serra_count"}}}}}},
{"eurfoil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.eur_foil", "$serra_count_foil"}}}}}}, {"eurfoil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.eur_foil", "$serra_count_foil"}}}}}},
{"usd", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.usd", "$serra_count"}}}}}}, {"usd", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.usd", "$serra_count"}}}}}},
{"usdetched", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.usd_etched", "$serra_count_etched"}}}}}},
{"usdfoil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.usd_foil", "$serra_count_foil"}}}}}}, {"usdfoil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.usd_foil", "$serra_count_foil"}}}}}},
}}} }}}
l.Info("Fetching bulk data from scryfall...") sets, _ := fetch_sets()
downloadURL, err := fetchBulkDownloadURL()
if err != nil {
l.Error("Could not extract bulk download URL:", err)
return err
}
l.Infof("Found latest bulkfile url: %s", downloadURL)
l.Info("Downloading bulk data file...")
bulkFilePath, err := downloadBulkData(downloadURL)
if err != nil {
l.Error("Could not fetch bulk json from scryfall", err)
return err
}
l.Info("Loading bulk data file...")
updatedCards, err := loadBulkFile(bulkFilePath)
if err != nil {
l.Error("Could not load bulk file:", err)
return err
}
l.Infof("Successfully loaded %d cards. Starting Update.", len(updatedCards))
sets, _ := fetchSets()
for _, set := range sets.Data { for _, set := range sets.Data {
// When downloading new sets, PriceList needs to be initialized // When downloading new sets, PriceList needs to be initialized
// This query silently fails if set was already downloaded. Not nice but ok for now. // This query silently fails if set was already downloaded. Not nice but ok for now.
// TODO: make this not fail silently
set.SerraPrices = []PriceEntry{} set.SerraPrices = []PriceEntry{}
setscoll.storageAddSet(&set) setscoll.storage_add_set(&set)
cards, _ := coll.storageFind(bson.D{{"set", set.Code}}, bson.D{{"_id", 1}}, 0, 0) cards, _ := coll.storage_find(bson.D{{"set", set.Code}}, bson.D{{"_id", 1}})
// if no cards in collection for this set, skip it // if no cards in collection for this set, skip it
if len(cards) == 0 { if len(cards) == 0 {
@ -98,25 +74,24 @@ var updateCmd = &cobra.Command{
SaucerHead: "[green]>[reset]", SaucerHead: "[green]>[reset]",
SaucerPadding: " ", SaucerPadding: " ",
BarStart: "|", BarStart: "|",
BarEnd: "| " + set.Name, BarEnd: fmt.Sprintf("| %s%s%s", Pink, set.Name, Reset),
}), }),
) )
for _, card := range cards { for _, card := range cards {
bar.Add(1) bar.Add(1)
updatedCard, err := getCardFromBulk(updatedCards, card.Set, card.CollectorNumber) updated_card, err := fetch_card(fmt.Sprintf("%s/%s", card.Set, card.CollectorNumber))
if err != nil { if err != nil {
l.Error(err) LogMessage(fmt.Sprintf("%v", err), "red")
continue continue
} }
updatedCard.Prices.Date = primitive.NewDateTimeFromTime(time.Now()) updated_card.Prices.Date = primitive.NewDateTimeFromTime(time.Now())
update := bson.M{ update := bson.M{
"$set": bson.M{"serra_updated": primitive.NewDateTimeFromTime(time.Now()), "prices": updatedCard.Prices, "cmc": updatedCard.Cmc, "cardmarketid": updatedCard.CardmarketID, "tcgplayerid": updatedCard.TCGPlayerID}, "$set": bson.M{"serra_updated": primitive.NewDateTimeFromTime(time.Now()), "prices": updated_card.Prices, "collectornumber": updated_card.CollectorNumber},
"$push": bson.M{"serra_prices": updatedCard.Prices}, "$push": bson.M{"serra_prices": updated_card.Prices},
} }
coll.storageUpdate(bson.M{"_id": bson.M{"$eq": card.ID}}, update) coll.storage_update(bson.M{"_id": bson.M{"$eq": card.ID}}, update)
} }
fmt.Println() fmt.Println()
@ -124,10 +99,10 @@ var updateCmd = &cobra.Command{
// calculate value summary // calculate value summary
matchStage := bson.D{{"$match", bson.D{{"set", set.Code}}}} matchStage := bson.D{{"$match", bson.D{{"set", set.Code}}}}
setValue, _ := coll.storageAggregate(mongo.Pipeline{matchStage, projectStage, groupStage}) setvalue, _ := coll.storage_aggregate(mongo.Pipeline{matchStage, projectStage, groupStage})
p := PriceEntry{} p := PriceEntry{}
s := setValue[0] s := setvalue[0]
p.Date = primitive.NewDateTimeFromTime(time.Now()) p.Date = primitive.NewDateTimeFromTime(time.Now())
@ -135,26 +110,27 @@ var updateCmd = &cobra.Command{
mapstructure.Decode(s, &p) mapstructure.Decode(s, &p)
// do the update // do the update
setUpdate := bson.M{ set_update := bson.M{
"$set": bson.M{"serra_updated": p.Date, "cardcount": set.CardCount}, "$set": bson.M{"serra_updated": p.Date},
"$push": bson.M{"serra_prices": p}, "$push": bson.M{"serra_prices": p},
} }
setscoll.storageUpdate(bson.M{"code": bson.M{"$eq": set.Code}}, setUpdate) // fmt.Printf("Set %s%s%s (%s) is now worth %s%.02f EUR%s\n", Pink, set.Name, Reset, set.Code, Yellow, setvalue[0]["value"], Reset)
setscoll.storage_update(bson.M{"code": bson.M{"$eq": set.Code}}, set_update)
} }
totalValue, _ := coll.storageAggregate(mongo.Pipeline{projectStage, groupStage}) totalvalue, _ := coll.storage_aggregate(mongo.Pipeline{projectStage, groupStage})
t := PriceEntry{} t := PriceEntry{}
t.Date = primitive.NewDateTimeFromTime(time.Now()) t.Date = primitive.NewDateTimeFromTime(time.Now())
mapstructure.Decode(totalValue[0], &t) mapstructure.Decode(totalvalue[0], &t)
// This is here to be able to fetch currency from // This is here to be able to fetch currency from
// constructed new priceentry // constructed new priceentry
tmpCard := Card{} tmpCard := Card{}
tmpCard.Prices = t tmpCard.Prices = t
l.Info("\n%sUpdating total value of collection to: %s%.02f%s%s\n", Green, Yellow, tmpCard.getValue(false)+tmpCard.getValue(true), getCurrency(), Reset) fmt.Printf("\n%sUpdating total value of collection to: %s%.02f %s%s\n", Green, Yellow, tmpCard.getValue(false)+tmpCard.getValue(true), getCurrency(), Reset)
totalcoll.storageAddTotal(t) totalcoll.storage_add_total(t)
return nil return nil
}, },

32
src/serra/utils.go Normal file
View File

@ -0,0 +1,32 @@
package serra
import "fmt"
var (
Icon = "\U0001F9D9\U0001F3FC"
Reset = "\033[0m"
Background = "\033[38;5;59m"
CurrentLine = "\033[38;5;60m"
Foreground = "\033[38;5;231m"
Comment = "\033[38;5;103m"
Cyan = "\033[38;5;159m"
Green = "\033[38;5;120m"
Orange = "\033[38;5;222m"
Pink = "\033[38;5;212m"
Purple = "\033[38;5;183m"
Red = "\033[38;5;210m"
Yellow = "\033[38;5;229m"
)
// Colored output on commandline
func LogMessage(message string, color string) {
if color == "red" {
fmt.Printf("%s%s%s\n", Red, message, Reset)
} else if color == "green" {
fmt.Printf("%s%s%s\n", Green, message, Reset)
} else if color == "purple" {
fmt.Printf("%s%s%s\n", Purple, message, Reset)
} else {
fmt.Printf("%s\n", message)
}
}

58
src/serra/web.go Normal file
View File

@ -0,0 +1,58 @@
package serra
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(webCmd)
}
var webCmd = &cobra.Command{
Aliases: []string{"a"},
Use: "web",
Short: "Startup web interface",
Long: "Start a tiny web interface to have a web view of your collection",
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
startWeb()
return nil
},
}
type Query struct {
Name string `form:"name"`
Set string `form:"set"`
Sort string `form:"sort"`
}
func startWeb() error {
router := gin.Default()
router.LoadHTMLGlob("templates/*.tmpl")
router.Static("/assets", "./assets")
// Landing Page
router.GET("/", landingPage)
router.Run(":8080")
return nil
}
func landingPage(c *gin.Context) {
var query Query
if c.ShouldBind(&query) == nil {
cards := Cards("", query.Set, query.Sort, query.Name, "", "")
sets := Sets("release")
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Serra",
"cards": cards,
"sets": sets,
"version": Version,
})
}
}

View File

@ -4,10 +4,9 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.title}}{{ if .query.Set }} - Set: {{.query.Set}}{{end}}</title> <title>Serra</title>
<link rel="stylesheet" href="https://jenil.github.io/bulmaswatch/cosmo/bulmaswatch.min.css"> <!-- <link rel="stylesheet" href="https://unpkg.com/bulma-dracula"> -->
<!-- <link rel="stylesheet" href="https://jenil.github.io/bulmaswatch/lumen/bulmaswatch.min.css"> --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css"> -->
<style> <style>
.cardpreview { .cardpreview {
@ -64,10 +63,10 @@
<body> <body>
<!-- Site Title --> <!-- Site Title -->
<section class="hero is-black"> <section class="hero is-primary">
<div class="hero-body"> <div class="hero-body">
<p class="title"> <p class="title">
<a href="/">{{ .title }}</a> {{ .title }}
</p> </p>
<p class="subtitle"> <p class="subtitle">
<i>Magic: The Gathering</i> Collection <i>Magic: The Gathering</i> Collection
@ -85,7 +84,7 @@
<div class="field"> <div class="field">
<label class="label">Set</label> <label class="label">Set</label>
<div class="control"> <div class="control">
<div class="select"> <div class="select is-primary">
<select name="set" id="set" form="searchform"> <select name="set" id="set" form="searchform">
<option value="">-</option> <option value="">-</option>
{{range .sets}} {{range .sets}}
@ -107,14 +106,11 @@
</div> </div>
</div> </div>
<input type="hidden" id="limit" name="limit" value="500" form="searchform">
<input type="hidden" id="page" name="page" value="0" form="searchform">
<div class="level-item"> <div class="level-item">
<div class="field"> <div class="field">
<label class="label">Sort</label> <label class="label">Sort</label>
<div class="control"> <div class="control">
<div class="select"> <div class="select is-primary">
<select name="sort" id="sort" form="searchform"> <select name="sort" id="sort" form="searchform">
<option value="name" selected>Name</option> <option value="name" selected>Name</option>
<option value="value">Value</option> <option value="value">Value</option>
@ -168,7 +164,7 @@
<tbody> <tbody>
{{range .cards}} {{range .cards}}
<tr> <tr>
<td>{{ add .SerraCount .SerraCountFoil }}</td> <td>{{.SerraCount}}</td>
<td> <td>
<div class="cardpreview"><strong>{{.Name }}</strong> <div class="cardpreview"><strong>{{.Name }}</strong>
<span class="cardpreviewtext"> <span class="cardpreviewtext">
@ -188,74 +184,19 @@
{{end}} {{end}}
</tbody> </tbody>
</table> </table>
</section> </section>
{{ if ne .numPages 0 }}
<div class="hero-body">
<nav class="pagination" role="navigation" aria-label="pagination">
{{ if ge .prevPage 0 }}
<a href="/?set={{.query.Set}}&name={{.query.Name}}&sort={{.query.Sort}}&limit={{.limit}}&page={{.prevPage}}" class="pagination-previous">Previous</a>
{{ end }}
{{ if ( le .nextPage .numPages) }}
<a href="/?set={{.query.Set}}&name={{.query.Name}}&sort={{.query.Sort}}&limit={{.limit}}&page={{.nextPage}}" class="pagination-next">Next page</a>
{{ end }}
<ul class="pagination-list">
{{ if ne .page 0 }}
<li>
<a class="pagination-link" href="/?set={{.query.Set}}&name={{.query.Name}}&sort={{.query.Sort}}&limit={{.limit}}&page=0" aria-label="Goto page 0">0</a>
</li>
<li>
<span class="pagination-ellipsis">&hellip;</span>
</li>
{{end}}
{{ if gt .prevPage 0 }}
<li>
<a href="/?set={{.query.Set}}&name={{.query.Name}}&sort={{.query.Sort}}&limit={{.limit}}&page={{.prevPage}}" class="pagination-link" aria-label="Goto page {{.prevPage}}">{{.prevPage}}</a>
</li>
{{end}}
<li>
<a class="pagination-link is-current" href="/?set={{.query.Set}}&name={{.query.Name}}&sort={{.query.Sort}}&limit={{.limit}}&page={{.page}}" aria-label="Page {{ .page }}" aria-current="page">{{.page}}</a>
</li>
{{ if and (ne .nextPage .numPages) ( lt .nextPage .numPages) }}
<li>
<a href="/?set={{.query.Set}}&name={{.query.Name}}&sort={{.query.Sort}}&limit={{.limit}}&page={{.nextPage}}" class="pagination-link" aria-label="Goto page {{.nextPage}} ">{{.nextPage}}</a>
</li>
{{ end }}
{{ if ne .numPages .page }}
<li>
<span class="pagination-ellipsis">&hellip;</span>
</li>
<li>
<a class="pagination-link" href="/?set={{.query.Set}}&name={{.query.Name}}&sort={{.query.Sort}}&limit={{.limit}}&page={{.numPages}}" aria-label="Goto page {{.numPages}}">{{.numPages}}</a>
</li>
{{end}}
</ul>
</nav>
</div>
{{ end }}
<!-- Select set from last search --> <!-- Select set from last search -->
<script> <script>
function getParam(paramName) { function getParam(paramName) {
return decodeURI( return decodeURI(
(RegExp(paramName + '=' + '(.+?)(&|$)').exec(location.search) || [, 500])[1] (RegExp(paramName + '=' + '(.+?)(&|$)').exec(location.search) || [, null])[1]
); );
} }
var selectedSetVal = getParam("set"); var selectedSetVal = getParam("set");
document.getElementById("set").value = selectedSetVal; document.getElementById("set").value = selectedSetVal;
var selectedLimitVal = getParam("limit");
document.getElementById("limit").value = selectedLimitVal;
var selectedSortVal = getParam("sort"); var selectedSortVal = getParam("sort");
document.getElementById("sort").value = selectedSortVal; document.getElementById("sort").value = selectedSortVal;
</script> </script>