Compare commits
142 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cea9a47de4 | |||
| 6a24cd6546 | |||
| b58718b537 | |||
| c4b4967777 | |||
| 3e4a977bef | |||
| bfa34d5c06 | |||
|
|
f75888a1a9 | ||
|
|
806137de61 | ||
|
|
34686494dd | ||
|
|
507eef148a | ||
|
|
a9d1fbc2cd | ||
|
|
3c593f5fdc | ||
|
|
7d23bca7f1 | ||
|
|
956ae53b59 | ||
|
|
7a1803df3a | ||
|
|
6e961a708c | ||
|
|
dab28a044a | ||
|
|
e2c7e54c16 | ||
|
|
d2728b455b | ||
|
|
cdc38ce2a0 | ||
|
|
7570d240ab | ||
|
|
7143eb84d2 | ||
|
|
541a4bbd19 | ||
|
|
4d0e75d806 | ||
|
|
fadb1c6aa4 | ||
|
|
1c48c5c1e4 | ||
|
|
0c575021b3 | ||
|
|
1229b9d2c6 | ||
|
|
515d32d491 | ||
|
|
a42a58f5b9 | ||
|
|
3794d1813f | ||
|
|
4d1fbf0255 | ||
|
|
23125bea5e | ||
|
|
f984c69100 | ||
|
|
3691890b95 | ||
|
|
1ce3920c03 | ||
|
|
29d6987275 | ||
|
|
7ad859704d | ||
|
|
51e139a964 | ||
|
|
6a88b536bf | ||
|
|
1be271715c | ||
|
|
7bee2d4540 | ||
|
|
542fdfd9a6 | ||
|
|
84122683c4 | ||
|
|
fd5067e66e | ||
|
|
ee42318b98 | ||
|
|
f2a2b4e65d | ||
|
|
c674e23d93 | ||
|
|
eb46a898cc | ||
|
|
2fe24d4781 | ||
|
|
3347adadaf | ||
|
|
af716ae755 | ||
|
|
0109d57bd9 | ||
|
|
c5b8ad7270 | ||
|
|
7e7bcd61e9 | ||
|
|
58a574b627 | ||
|
|
1f5e574ca3 | ||
|
|
ed7294cadb | ||
|
|
240342d52f | ||
|
|
f1c3de2836 | ||
|
|
64607f3680 | ||
|
|
f34e49ed71 | ||
|
|
b342a5df75 | ||
|
|
dcbd275c18 | ||
|
|
4cb840558f | ||
|
|
07e9a962d2 | ||
|
|
0b72fa0265 | ||
|
|
c7052c9aec | ||
|
|
5578df8e19 | ||
|
|
a4268afb7f | ||
|
|
5fb1cb63a2 | ||
|
|
135f4a93a5 | ||
|
|
71259001d3 | ||
|
|
55e2f72b27 | ||
|
|
ff5575b579 | ||
|
|
cbfbfeef9e | ||
|
|
ef33194e16 | ||
|
|
f0e90350b9 | ||
|
|
3870dc8172 | ||
|
|
23006311d2 | ||
|
|
446936496f | ||
|
|
b4f0067b4a | ||
|
|
884fbdf96d | ||
|
|
10e84868e7 | ||
|
|
ca04ee1ab1 | ||
|
|
f0c82cf325 | ||
|
|
2de8d089b8 | ||
|
|
4580748ebe | ||
|
|
5eac08c40b | ||
|
|
5f3d643b92 | ||
|
|
924c222fe5 | ||
|
|
697365c518 | ||
|
|
f34aa42e8f | ||
|
|
5f135d07cb | ||
|
|
8ccaafc140 | ||
|
|
0140463e09 | ||
|
|
d1dac680e0 | ||
|
|
bb823aeb20 | ||
|
|
d704528d8e | ||
|
|
c6e35c48c9 | ||
|
|
c20f84562e | ||
|
|
510e7fe9da | ||
|
|
f0fc422e30 | ||
|
|
73e982ab92 | ||
|
|
d1fd624ba8 | ||
|
|
71f451bbd1 | ||
|
|
d293586eba | ||
|
|
d03931c091 | ||
|
|
030bc2c3d8 | ||
|
|
af4af39adc | ||
|
|
7b6addcb05 | ||
|
|
578ad203ca | ||
|
|
d9bb463be9 | ||
|
|
104b9b6e96 | ||
|
|
af8a6af2ec | ||
|
|
27be6bf772 | ||
|
|
339cb56eb2 | ||
|
|
03a15d998e | ||
|
|
40b08d9846 | ||
|
|
6965317ea5 | ||
|
|
8e22505ca0 | ||
|
|
fa59a96db2 | ||
|
|
38ffc25b44 | ||
|
|
96731b8fe0 | ||
|
|
755b333bbb | ||
|
|
ba6ae6db71 | ||
|
|
040561c462 | ||
|
|
f2f6317234 | ||
|
|
8258bca36b | ||
|
|
e4b5f87be8 | ||
|
|
637d385e74 | ||
|
|
2ed9467f6a | ||
|
|
e0d90ae12d | ||
|
|
c784477241 | ||
|
|
4a1147b1af | ||
|
|
c106a01a28 | ||
|
|
0d51645516 | ||
|
|
927c568059 | ||
|
|
5c3c06d1fc | ||
|
|
ed1bb5c5a4 | ||
|
|
f6621e9bd8 | ||
|
|
de868f58b1 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,7 +1,6 @@
|
||||
.DS_Store
|
||||
serra
|
||||
!src/serra
|
||||
_db/*
|
||||
/serra
|
||||
!_db/.placeholder
|
||||
_backup/*
|
||||
!_backup/.placeholder
|
||||
|
||||
51
.goreleaser.yaml
Normal file
51
.goreleaser.yaml
Normal file
@ -0,0 +1,51 @@
|
||||
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"
|
||||
|
||||
@ -1,97 +0,0 @@
|
||||
# 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"
|
||||
|
||||
@ -1 +1 @@
|
||||
golang 1.20
|
||||
golang 1.21.3
|
||||
|
||||
15
Dockerfile
15
Dockerfile
@ -1,23 +1,24 @@
|
||||
FROM golang:alpine AS builder
|
||||
FROM golang:1.25-alpine AS build
|
||||
|
||||
RUN apk update && apk add --no-cache git
|
||||
RUN apk update && apk add --no-cache git ca-certificates curl
|
||||
|
||||
WORKDIR /go/src/app
|
||||
COPY src /go/src/app/src
|
||||
COPY pkg /go/src/app/pkg
|
||||
COPY cmd /go/src/app/cmd
|
||||
COPY templates /go/src/app/templates
|
||||
COPY go.mod /go/src/app/go.mod
|
||||
COPY go.sum /go/src/app/go.sum
|
||||
COPY .git /go/src/app/.git
|
||||
COPY serra.go /go/src/app/serra.go
|
||||
|
||||
# build
|
||||
RUN go get -v ./...
|
||||
RUN go build -ldflags "-X github.com/noqqe/serra/src/serra.Version=`git describe --tags`" -v serra.go
|
||||
RUN go build -ldflags "-X github.com/noqqe/serra/pkg/serra.Version=`git describe --tags`" -v cmd/serra/serra.go
|
||||
|
||||
# copy
|
||||
FROM scratch
|
||||
WORKDIR /go/src/app
|
||||
COPY --from=builder /go/src/app/serra /go/src/app/serra
|
||||
COPY --from=build /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
|
||||
|
||||
# run
|
||||
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
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.
|
||||
@ -5,17 +5,17 @@ version: '3'
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- go build -ldflags "-X github.com/noqqe/serra/src/serra.Version=`git describe --tags`" -v serra.go
|
||||
- go build -ldflags "-X github.com/noqqe/serra/pkg/serra.Version=`git describe --tags`" -v cmd/serra/serra.go
|
||||
sources:
|
||||
- "src/serra/**/*.go"
|
||||
- "serra.go"
|
||||
- "pkg/serra/**/*.go"
|
||||
- "cmd/serra/serra.go"
|
||||
generates:
|
||||
- "./serra"
|
||||
|
||||
release:
|
||||
interactive: true
|
||||
cmds:
|
||||
- git tag | tail -5
|
||||
- git tag | sort -t. -k 1,1n -k 2,2n -k 3,3n | tail -5
|
||||
- read -p "Version v1.1.1 " version ; git tag $version
|
||||
- git push --tags
|
||||
- goreleaser release --clean
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Package main provides a typing test
|
||||
package main
|
||||
|
||||
import "github.com/noqqe/serra/src/serra"
|
||||
import "github.com/noqqe/serra/pkg/serra"
|
||||
|
||||
func main() {
|
||||
serra.Execute()
|
||||
@ -2,7 +2,7 @@ version: '3.6'
|
||||
|
||||
services:
|
||||
mongo:
|
||||
image: mongo
|
||||
image: mongo:6
|
||||
restart: always
|
||||
ports:
|
||||
- 27017:27017
|
||||
|
||||
76
go.mod
76
go.mod
@ -1,23 +1,67 @@
|
||||
module github.com/noqqe/serra
|
||||
|
||||
go 1.14
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.25.4
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/log v0.4.0
|
||||
github.com/chzyer/readline v1.5.1
|
||||
github.com/gin-gonic/gin v1.9.0
|
||||
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/klauspost/compress v1.15.15 // indirect
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/montanaflynn/stats v0.7.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/schollz/progressbar/v3 v3.13.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
|
||||
go.mongodb.org/mongo-driver v1.11.3
|
||||
golang.org/x/crypto v0.6.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
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/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
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/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
golang.org/x/arch v0.11.0 // indirect
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // 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
207
go.sum
@ -1,195 +1,180 @@
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
|
||||
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
|
||||
github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
|
||||
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/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
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/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
|
||||
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
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/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/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
|
||||
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
|
||||
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
|
||||
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/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
||||
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/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/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
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/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||
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/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/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/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/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/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 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-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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU=
|
||||
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.3/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/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/schollz/progressbar/v3 v3.13.0 h1:9TeeWRcjW2qd05I8Kf9knPkW4vLM/hYoa6z9ABvxje8=
|
||||
github.com/schollz/progressbar/v3 v3.13.0/go.mod h1:ZBYnSuLAX2LU8P8UiKN/KgF2DY58AJC8yfVYLPC8Ly4=
|
||||
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||
github.com/schollz/progressbar/v3 v3.16.1 h1:RnF1neWZFzLCoGx8yp1yF7SDl4AzNDI5y4I0aUJRrZQ=
|
||||
github.com/schollz/progressbar/v3 v3.16.1/go.mod h1:I2ILR76gz5VXqYMIY/LdLecvMHDPVcQm3W/MSKi1TME=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
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/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.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
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.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
|
||||
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
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/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/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/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
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/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mongodb.org/mongo-driver v1.11.3 h1:Ql6K6qYHEzB6xvu4+AU0BoRoqf9vFPcc4o7MUIdPW8Y=
|
||||
go.mongodb.org/mongo-driver v1.11.3/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM=
|
||||
go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
|
||||
golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
|
||||
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
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-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
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-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.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
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/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
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.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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-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-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-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.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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
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.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.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
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/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
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.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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
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=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
||||
BIN
imgs/check.png
Normal file
BIN
imgs/check.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
190
pkg/serra/add.go
Normal file
190
pkg/serra/add.go
Normal file
@ -0,0 +1,190 @@
|
||||
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
|
||||
}
|
||||
243
pkg/serra/card.go
Normal file
243
pkg/serra/card.go
Normal file
@ -0,0 +1,243 @@
|
||||
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()
|
||||
}
|
||||
77
pkg/serra/check.go
Normal file
77
pkg/serra/check.go
Normal file
@ -0,0 +1,77 @@
|
||||
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
|
||||
}
|
||||
67
pkg/serra/deck.go
Normal file
67
pkg/serra/deck.go
Normal file
@ -0,0 +1,67 @@
|
||||
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
|
||||
}
|
||||
33
pkg/serra/env.go
Normal file
33
pkg/serra/env.go
Normal file
@ -0,0 +1,33 @@
|
||||
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 "$"
|
||||
}
|
||||
}
|
||||
127
pkg/serra/export.go
Normal file
127
pkg/serra/export.go
Normal file
@ -0,0 +1,127 @@
|
||||
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))
|
||||
}
|
||||
@ -2,7 +2,6 @@ package serra
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
@ -13,9 +12,11 @@ func init() {
|
||||
rootCmd.AddCommand(topsCmd)
|
||||
rootCmd.AddCommand(flopsCmd)
|
||||
topsCmd.Flags().Float64VarP(&limit, "limit", "l", 0, "Minimum card price to be shown in analysis")
|
||||
topsCmd.Flags().StringVarP(&since, "since", "s", "0", "Since when should the gains be calculated")
|
||||
topsCmd.Flags().BoolVarP(&sinceLastUpdate, "since-last-update", "u", false, "Show gains since last update")
|
||||
topsCmd.Flags().BoolVarP(&sinceBeginning, "since-beginning", "b", true, "Show gains since beginning of records")
|
||||
flopsCmd.Flags().Float64VarP(&limit, "limit", "l", 0, "Minimum card price to be shown in analysis")
|
||||
flopsCmd.Flags().StringVarP(&since, "since", "s", "0", "Since when should the losses be calculated")
|
||||
flopsCmd.Flags().BoolVarP(&sinceLastUpdate, "since-last-update", "u", false, "Show losses since last update")
|
||||
flopsCmd.Flags().BoolVarP(&sinceBeginning, "since-beginning", "b", true, "Show losses since beginning of records")
|
||||
}
|
||||
|
||||
var topsCmd = &cobra.Command{
|
||||
@ -24,7 +25,7 @@ var topsCmd = &cobra.Command{
|
||||
Short: "What cards gained most value",
|
||||
SilenceErrors: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
Gains(limit, -1, since)
|
||||
Gains(limit, -1)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@ -35,26 +36,32 @@ var flopsCmd = &cobra.Command{
|
||||
Short: "What cards lost most value",
|
||||
SilenceErrors: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
Gains(limit, 1, since)
|
||||
Gains(limit, 1)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func Gains(limit float64, sort int, since string) error {
|
||||
func Gains(limit float64, sort int) error {
|
||||
|
||||
client := storage_connect()
|
||||
client := storageConnect()
|
||||
coll := &Collection{client.Database("serra").Collection("cards")}
|
||||
setcoll := &Collection{client.Database("serra").Collection("sets")}
|
||||
defer storage_disconnect(client)
|
||||
defer storageDisconnect(client)
|
||||
|
||||
old, _ := strconv.Atoi(since)
|
||||
var old int
|
||||
if sinceBeginning {
|
||||
old = 0
|
||||
}
|
||||
if sinceLastUpdate {
|
||||
old = -2
|
||||
}
|
||||
|
||||
currencyField := "$serra_prices.usd"
|
||||
if getCurrency() == "EUR" {
|
||||
if getCurrency() == EUR {
|
||||
currencyField = "$serra_prices.eur"
|
||||
}
|
||||
|
||||
raise_pipeline := mongo.Pipeline{
|
||||
raisePipeline := mongo.Pipeline{
|
||||
bson.D{{"$project",
|
||||
bson.D{
|
||||
{"name", true},
|
||||
@ -102,9 +109,9 @@ func Gains(limit float64, sort int, since string) error {
|
||||
bson.D{{"rate", sort}}}},
|
||||
bson.D{{"$limit", 20}},
|
||||
}
|
||||
raise, _ := coll.storage_aggregate(raise_pipeline)
|
||||
raise, _ := coll.storageAggregate(raisePipeline)
|
||||
|
||||
sraise_pipeline := mongo.Pipeline{
|
||||
sraisePipeline := mongo.Pipeline{
|
||||
bson.D{{"$project",
|
||||
bson.D{
|
||||
{"name", true},
|
||||
@ -150,25 +157,25 @@ func Gains(limit float64, sort int, since string) error {
|
||||
bson.D{{"rate", sort}}}},
|
||||
bson.D{{"$limit", 10}},
|
||||
}
|
||||
sraise, _ := setcoll.storage_aggregate(sraise_pipeline)
|
||||
sraise, _ := setcoll.storageAggregate(sraisePipeline)
|
||||
|
||||
// percentage coloring
|
||||
var p_color string
|
||||
var pColor string
|
||||
if sort == 1 {
|
||||
p_color = Red
|
||||
pColor = Red
|
||||
} else {
|
||||
p_color = Green
|
||||
pColor = Green
|
||||
}
|
||||
|
||||
fmt.Printf("%sCards%s\n", Purple, Reset)
|
||||
// print each card
|
||||
for _, e := range raise {
|
||||
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("%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("\n%sSets%s\n", Purple, Reset)
|
||||
for _, e := range sraise {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
return nil
|
||||
|
||||
323
pkg/serra/helpers.go
Normal file
323
pkg/serra/helpers.go
Normal file
@ -0,0 +1,323 @@
|
||||
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'
|
||||
}
|
||||
84
pkg/serra/missing.go
Normal file
84
pkg/serra/missing.go
Normal file
@ -0,0 +1,84 @@
|
||||
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
|
||||
},
|
||||
}
|
||||
@ -2,7 +2,6 @@ package serra
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
@ -14,6 +13,7 @@ func init() {
|
||||
removeCmd.Flags().Int64VarP(&count, "count", "c", 1, "Amount of cards to remove")
|
||||
removeCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Spin up interactive terminal")
|
||||
removeCmd.Flags().StringVarP(&set, "set", "s", "", "Filter by set code (usg/mmq/vow)")
|
||||
removeCmd.Flags().BoolVarP(&foil, "foil", "f", false, "Remove foil variant of card")
|
||||
rootCmd.AddCommand(removeCmd)
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ var removeCmd = &cobra.Command{
|
||||
RunE: func(cmd *cobra.Command, cards []string) error {
|
||||
|
||||
if interactive {
|
||||
removeCardsInteractive(unique, set)
|
||||
removeCardsInteractive(set)
|
||||
} else {
|
||||
removeCards(cards, count)
|
||||
}
|
||||
@ -34,11 +34,11 @@ var removeCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func removeCardsInteractive(unique bool, set string) {
|
||||
func removeCardsInteractive(set string) {
|
||||
l := Logger()
|
||||
|
||||
if len(set) == 0 {
|
||||
LogMessage("Error: --set must be given in interactive mode", "red")
|
||||
os.Exit(1)
|
||||
l.Fatal("Option --set must be given in interactive mode")
|
||||
}
|
||||
|
||||
rl, err := readline.New(fmt.Sprintf("%s> ", set))
|
||||
@ -63,27 +63,57 @@ func removeCardsInteractive(unique bool, set string) {
|
||||
}
|
||||
|
||||
func removeCards(cards []string, count int64) error {
|
||||
|
||||
client := storage_connect()
|
||||
// Connect to the DB & load the collection
|
||||
client := storageConnect()
|
||||
coll := &Collection{client.Database("serra").Collection("cards")}
|
||||
defer storage_disconnect(client)
|
||||
l := Logger()
|
||||
defer storageDisconnect(client)
|
||||
|
||||
// Loop over different cards
|
||||
for _, card := range cards {
|
||||
// Fetch card from scryfall
|
||||
c, err := find_card_by_setcollectornumber(coll, strings.Split(card, "/")[0], strings.Split(card, "/")[1])
|
||||
if err != nil {
|
||||
LogMessage(fmt.Sprintf("%v", err), "red")
|
||||
|
||||
if !strings.Contains(card, "/") {
|
||||
l.Errorf("Invalid card format %s. Needs to be set/collector number i.e. \"usg/13\"", card)
|
||||
continue
|
||||
}
|
||||
|
||||
if c.SerraCount > 1 {
|
||||
modify_count_of_card(coll, c, -1)
|
||||
} else {
|
||||
coll.storage_remove(bson.M{"_id": c.ID})
|
||||
LogMessage(fmt.Sprintf("\"%s\" (%.2f %s) removed from the Collection.", c.Name, c.getValue(), getCurrency()), "green")
|
||||
// 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
|
||||
c, err := findCardByCollectorNumber(coll, setName, collectorNumber)
|
||||
if err != nil {
|
||||
l.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
if foil && c.SerraCountFoil < 1 {
|
||||
l.Errorf("No foil \"%s\" in the collection", c.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if !foil && c.SerraCount < 1 {
|
||||
l.Errorf("No normal \"%s\" in the collection", c.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if !askConfirmation(c) {
|
||||
continue
|
||||
}
|
||||
|
||||
if foil && c.SerraCountFoil == 1 && c.SerraCount == 0 || !foil && c.SerraCount == 1 && c.SerraCountFoil == 0 {
|
||||
coll.storageRemove(bson.M{"_id": c.ID})
|
||||
l.Infof("\"%s\" (%.2f%s) removed", c.Name, c.getValue(foil), getCurrency())
|
||||
} else {
|
||||
modifyCardCount(coll, c, -count, foil)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
52
pkg/serra/root.go
Normal file
52
pkg/serra/root.go
Normal file
@ -0,0 +1,52 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@ -2,13 +2,14 @@ package serra
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
@ -16,19 +17,24 @@ import (
|
||||
|
||||
type Card struct {
|
||||
// Added by Serra
|
||||
SerraCount int64 `bson:"serra_count"`
|
||||
SerraCountFoil int64 `bson:"serra_count_foil"`
|
||||
SerraCountEtched int64 `bson:"serra_count_etched"`
|
||||
SerraPrices []PriceEntry `bson:"serra_prices"`
|
||||
SerraCreated primitive.DateTime `bson:"serra_created"`
|
||||
SerraUpdated primitive.DateTime `bson:"serra_updated"`
|
||||
SerraCount int64 `bson:"serra_count"`
|
||||
SerraCountFoil int64 `bson:"serra_count_foil"`
|
||||
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"`
|
||||
SerraCreated primitive.DateTime `bson:"serra_created"`
|
||||
SerraUpdated primitive.DateTime `bson:"serra_updated"`
|
||||
SerraImage64 string `bson:"serra_image"`
|
||||
|
||||
Artist string `json:"artist"`
|
||||
ArtistIds []string `json:"artist_ids"`
|
||||
Booster bool `json:"booster"`
|
||||
BorderColor string `json:"border_color"`
|
||||
CardBackID string `json:"card_back_id"`
|
||||
Cmc int64 `json:"cmc"`
|
||||
CardmarketID float64 `json:"cardmarket_id"`
|
||||
Cmc float64 `json:"cmc"`
|
||||
CollectorNumber string `json:"collector_number"`
|
||||
ColorIdentity []string `json:"color_identity"`
|
||||
Colors []string `json:"colors"`
|
||||
@ -100,31 +106,139 @@ type Card struct {
|
||||
TcgplayerInfiniteArticles string `json:"tcgplayer_infinite_articles"`
|
||||
TcgplayerInfiniteDecks string `json:"tcgplayer_infinite_decks"`
|
||||
} `json:"related_uris"`
|
||||
ReleasedAt string `json:"released_at"`
|
||||
Reprint bool `json:"reprint"`
|
||||
Reserved bool `json:"reserved"`
|
||||
RulingsURI string `json:"rulings_uri"`
|
||||
ScryfallSetURI string `json:"scryfall_set_uri"`
|
||||
ScryfallURI string `json:"scryfall_uri"`
|
||||
Set string `json:"set"`
|
||||
SetID string `json:"set_id"`
|
||||
SetName string `json:"set_name"`
|
||||
SetSearchURI string `json:"set_search_uri"`
|
||||
SetType string `json:"set_type"`
|
||||
SetURI string `json:"set_uri"`
|
||||
StorySpotlight bool `json:"story_spotlight"`
|
||||
Textless bool `json:"textless"`
|
||||
TypeLine string `json:"type_line"`
|
||||
URI string `json:"uri"`
|
||||
Variation bool `json:"variation"`
|
||||
ReleasedAt string `json:"released_at"`
|
||||
Reprint bool `json:"reprint"`
|
||||
Reserved bool `json:"reserved"`
|
||||
RulingsURI string `json:"rulings_uri"`
|
||||
ScryfallSetURI string `json:"scryfall_set_uri"`
|
||||
ScryfallURI string `json:"scryfall_uri"`
|
||||
Set string `json:"set"`
|
||||
SetID string `json:"set_id"`
|
||||
SetName string `json:"set_name"`
|
||||
SetSearchURI string `json:"set_search_uri"`
|
||||
SetType string `json:"set_type"`
|
||||
SetURI string `json:"set_uri"`
|
||||
StorySpotlight bool `json:"story_spotlight"`
|
||||
Textless bool `json:"textless"`
|
||||
TCGPlayerID float64 `json:"tcgplayer_id"`
|
||||
TypeLine string `json:"type_line"`
|
||||
URI string `json:"uri"`
|
||||
Variation bool `json:"variation"`
|
||||
}
|
||||
|
||||
// Getter for currency specific value
|
||||
func (c Card) getValue() float64 {
|
||||
if getCurrency() == "EUR" {
|
||||
return c.Prices.Eur
|
||||
type BulkIndex struct {
|
||||
Object string `json:"object"`
|
||||
HasMore bool `json:"has_more"`
|
||||
Data []struct {
|
||||
Object string `json:"object"`
|
||||
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)
|
||||
}
|
||||
return c.Prices.Usd
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read the response body
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalf("Error reading response body: %v", err)
|
||||
}
|
||||
|
||||
// Unmarshal the JSON response
|
||||
var bulkData BulkIndex
|
||||
if err := json.Unmarshal(body, &bulkData); err != nil {
|
||||
log.Fatalf("Error unmarshaling JSON: %v", err)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@ -164,29 +278,35 @@ type Set struct {
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
|
||||
func fetch_card(path string) (*Card, error) {
|
||||
|
||||
if !strings.Contains(path, "/") {
|
||||
err := errors.New(fmt.Sprintf("Card must follow format <set>/<number>, for example: ath/15"))
|
||||
return &Card{}, err
|
||||
// Getter for currency specific value
|
||||
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
|
||||
}
|
||||
|
||||
// TODO better URL Building...
|
||||
resp, err := http.Get(fmt.Sprintf("https://api.scryfall.com/cards/%s/", path))
|
||||
func fetchCard(setName, collectorNumber string) (*Card, error) {
|
||||
resp, err := http.Get(fmt.Sprintf("https://api.scryfall.com/cards/%s/%s/", setName, collectorNumber))
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
return &Card{}, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
err := errors.New(fmt.Sprintf("Error: %s not found", path))
|
||||
return &Card{}, err
|
||||
return &Card{}, fmt.Errorf("Card %s/%s not found", setName, collectorNumber)
|
||||
}
|
||||
|
||||
//We Read the response body on the line below.
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
//we read the response body on the line below.
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
log.Fatalf("%s", err)
|
||||
return &Card{}, err
|
||||
}
|
||||
|
||||
@ -194,6 +314,9 @@ func fetch_card(path string) (*Card, error) {
|
||||
decoder := json.NewDecoder(r)
|
||||
val := &Card{}
|
||||
err = decoder.Decode(val)
|
||||
if err != nil {
|
||||
log.Fatalf("%s", err)
|
||||
}
|
||||
|
||||
// Set created Time
|
||||
val.SerraCreated = primitive.NewDateTimeFromTime(time.Now())
|
||||
@ -202,24 +325,36 @@ func fetch_card(path string) (*Card, error) {
|
||||
val.Prices.Date = primitive.NewDateTimeFromTime(time.Now())
|
||||
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
|
||||
}
|
||||
|
||||
func fetch_sets() (*SetList, error) {
|
||||
// TODO better URL Building...
|
||||
resp, err := http.Get(fmt.Sprintf("https://api.scryfall.com/sets"))
|
||||
func fetchSets() (*SetList, error) {
|
||||
// TODO: better URL Building...
|
||||
resp, err := http.Get("https://api.scryfall.com/sets")
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
return &SetList{}, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
err := errors.New(fmt.Sprintf("Error: /sets not found"))
|
||||
return &SetList{}, err
|
||||
return &SetList{}, fmt.Errorf("/sets not found")
|
||||
}
|
||||
|
||||
//We Read the response body on the line below.
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
return &SetList{}, err
|
||||
@ -228,7 +363,11 @@ func fetch_sets() (*SetList, error) {
|
||||
r := bytes.NewReader(body)
|
||||
decoder := json.NewDecoder(r)
|
||||
val := &SetList{}
|
||||
|
||||
err = decoder.Decode(val)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
@ -25,7 +25,7 @@ otherwise you'll get a list of sets as a search result.`,
|
||||
RunE: func(cmd *cobra.Command, set []string) error {
|
||||
if len(set) == 0 {
|
||||
setList := Sets(sortby)
|
||||
show_set_list(setList)
|
||||
showSetList(setList)
|
||||
} else {
|
||||
ShowSet(set[0])
|
||||
}
|
||||
@ -35,14 +35,15 @@ otherwise you'll get a list of sets as a search result.`,
|
||||
|
||||
func Sets(sort string) []primitive.M {
|
||||
|
||||
client := storage_connect()
|
||||
client := storageConnect()
|
||||
coll := &Collection{client.Database("serra").Collection("cards")}
|
||||
defer storage_disconnect(client)
|
||||
defer storageDisconnect(client)
|
||||
|
||||
groupStage := bson.D{
|
||||
{"$group", bson.D{
|
||||
{"_id", "$setname"},
|
||||
{"value", bson.D{{"$sum", bson.D{{"$multiply", bson.A{getCurrencyField(), "$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"}}}}}},
|
||||
{"count", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count"}}}}}},
|
||||
{"unique", bson.D{{"$sum", 1}}},
|
||||
{"code", bson.D{{"$last", "$set"}}},
|
||||
@ -64,45 +65,46 @@ func Sets(sort string) []primitive.M {
|
||||
}}}
|
||||
}
|
||||
|
||||
sets, _ := coll.storage_aggregate(mongo.Pipeline{groupStage, sortStage})
|
||||
sets, _ := coll.storageAggregate(mongo.Pipeline{groupStage, sortStage})
|
||||
return sets
|
||||
|
||||
}
|
||||
|
||||
func show_set_list(sets []primitive.M) {
|
||||
func showSetList(sets []primitive.M) {
|
||||
|
||||
client := storage_connect()
|
||||
client := storageConnect()
|
||||
setscoll := &Collection{client.Database("serra").Collection("sets")}
|
||||
|
||||
for _, set := range sets {
|
||||
setobj, _ := find_set_by_code(setscoll, set["code"].(string))
|
||||
setobj, _ := findSetByCode(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(" 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()
|
||||
}
|
||||
}
|
||||
|
||||
func ShowSet(setname string) error {
|
||||
|
||||
client := storage_connect()
|
||||
client := storageConnect()
|
||||
coll := &Collection{client.Database("serra").Collection("cards")}
|
||||
defer storage_disconnect(client)
|
||||
l := Logger()
|
||||
defer storageDisconnect(client)
|
||||
|
||||
// fetch all cards in set ordered by currently used currency
|
||||
cardSortCurrency := bson.D{{"prices.usd", -1}}
|
||||
if getCurrency() == "EUR" {
|
||||
if getCurrency() == EUR {
|
||||
cardSortCurrency = bson.D{{"prices.eur", -1}}
|
||||
}
|
||||
cards, err := coll.storage_find(bson.D{{"set", setname}}, cardSortCurrency)
|
||||
cards, err := coll.storageFind(bson.D{{"set", setname}}, cardSortCurrency, 0, 0)
|
||||
if (err != nil) || len(cards) == 0 {
|
||||
LogMessage(fmt.Sprintf("Error: Set %s not found or no card in your collection.", setname), "red")
|
||||
l.Errorf("Set %s not found or no card in your collection.", setname)
|
||||
return err
|
||||
}
|
||||
|
||||
// fetch set informations
|
||||
setcoll := &Collection{client.Database("serra").Collection("sets")}
|
||||
sets, _ := setcoll.storage_find_set(bson.D{{"code", setname}}, bson.D{{"_id", 1}})
|
||||
sets, _ := setcoll.storageFindSet(bson.D{{"code", setname}}, bson.D{{"_id", 1}})
|
||||
|
||||
// set values
|
||||
matchStage := bson.D{
|
||||
@ -113,11 +115,13 @@ func ShowSet(setname string) error {
|
||||
groupStage := bson.D{
|
||||
{"$group", bson.D{
|
||||
{"_id", "$setname"},
|
||||
{"value", bson.D{{"$sum", bson.D{{"$multiply", bson.A{getCurrencyField(), "$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"}}}}}},
|
||||
{"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.storage_aggregate(mongo.Pipeline{matchStage, groupStage})
|
||||
stats, _ := coll.storageAggregate(mongo.Pipeline{matchStage, groupStage})
|
||||
|
||||
// set rarities
|
||||
matchStage = bson.D{
|
||||
@ -135,21 +139,44 @@ func ShowSet(setname string) error {
|
||||
{"$sort", bson.D{
|
||||
{"_id", 1},
|
||||
}}}
|
||||
rar, _ := coll.storage_aggregate(mongo.Pipeline{matchStage, groupStage, sortStage})
|
||||
rar, _ := coll.storageAggregate(mongo.Pipeline{matchStage, groupStage, sortStage})
|
||||
|
||||
ri := convert_rarities(rar)
|
||||
ri := convertRarities(rar)
|
||||
|
||||
fmt.Printf("%s%s%s\n", Green, sets[0].Name, Reset)
|
||||
fmt.Printf("Released: %s\n", sets[0].ReleasedAt)
|
||||
fmt.Printf("Set Cards: %d/%d\n", len(cards), sets[0].CardCount)
|
||||
fmt.Printf("Total Cards: %.0f\n", stats[0]["count"])
|
||||
fmt.Printf("Foil Cards: %.0f\n", stats[0]["count_foil"])
|
||||
|
||||
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
|
||||
}
|
||||
totalValue := normalValue + foilValue
|
||||
|
||||
normalCount, _ := getFloat64(stats[0]["count"])
|
||||
foilCount, _ := getFloat64(stats[0]["count_foil"])
|
||||
|
||||
fmt.Printf("\n%sCurrent Value%s\n", Purple, Reset)
|
||||
fmt.Printf("Total: %.0fx %s%.2f%s%s\n", normalCount+foilCount, Yellow, totalValue, getCurrency(), Reset)
|
||||
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)
|
||||
|
||||
LogMessage(fmt.Sprintf("%s", sets[0].Name), "green")
|
||||
LogMessage(fmt.Sprintf("Set Cards: %d/%d", len(cards), sets[0].CardCount), "normal")
|
||||
LogMessage(fmt.Sprintf("Total Cards: %.0f", stats[0]["count"]), "normal")
|
||||
LogMessage(fmt.Sprintf("Total Value: %.2f %s", stats[0]["value"], getCurrency()), "normal")
|
||||
LogMessage(fmt.Sprintf("Released: %s", sets[0].ReleasedAt), "normal")
|
||||
LogMessage(fmt.Sprintf("Mythics: %.0f", ri.Mythics), "normal")
|
||||
LogMessage(fmt.Sprintf("Rares: %.0f", ri.Rares), "normal")
|
||||
LogMessage(fmt.Sprintf("Uncommons: %.0f", ri.Uncommons), "normal")
|
||||
LogMessage(fmt.Sprintf("Commons: %.0f", ri.Commons), "normal")
|
||||
fmt.Printf("\n%sPrice History:%s\n", Pink, Reset)
|
||||
print_price_history(sets[0].SerraPrices, "* ")
|
||||
showPriceHistory(sets[0].SerraPrices, "* ", true)
|
||||
|
||||
fmt.Printf("\n%sMost valuable cards%s\n", Pink, Reset)
|
||||
|
||||
@ -163,7 +190,7 @@ func ShowSet(setname string) error {
|
||||
|
||||
for i := 0; i < ccards; i++ {
|
||||
card := cards[i]
|
||||
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(), getCurrency(), Reset)
|
||||
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)
|
||||
}
|
||||
|
||||
return nil
|
||||
245
pkg/serra/stats.go
Normal file
245
pkg/serra/stats.go
Normal file
@ -0,0 +1,245 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@ package serra
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
@ -18,38 +17,48 @@ type Total struct {
|
||||
Value []PriceEntry `bson:"value"`
|
||||
}
|
||||
|
||||
// https://siongui.github.io/2017/02/11/go-add-method-function-to-type-in-external-package/
|
||||
// Collection Struct
|
||||
// reason: https://siongui.github.io/2017/02/11/go-add-method-function-to-type-in-external-package/
|
||||
type Collection struct {
|
||||
*mongo.Collection
|
||||
}
|
||||
|
||||
// Returns configured human readable name for
|
||||
// the configured currency of the user
|
||||
func getCurrencyField() string {
|
||||
func getCurrencyField(foil bool) string {
|
||||
switch os.Getenv("SERRA_CURRENCY") {
|
||||
case "EUR":
|
||||
if foil {
|
||||
return "$prices.eur_foil"
|
||||
}
|
||||
return "$prices.eur"
|
||||
case "USD":
|
||||
if foil {
|
||||
return "$prices.usd_foil"
|
||||
}
|
||||
return "$prices.usd"
|
||||
default:
|
||||
if foil {
|
||||
return "$prices.usd_foil"
|
||||
}
|
||||
return "$prices.usd"
|
||||
}
|
||||
}
|
||||
|
||||
func storage_connect() *mongo.Client {
|
||||
|
||||
func storageConnect() *mongo.Client {
|
||||
l := Logger()
|
||||
uri := getMongoDBURI()
|
||||
|
||||
client, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(uri))
|
||||
if err != nil {
|
||||
LogMessage(fmt.Sprintf("Could not connect to mongodb at %s", uri), "red")
|
||||
os.Exit(1)
|
||||
l.Fatalf("Could not connect to mongodb at %s", uri)
|
||||
}
|
||||
|
||||
return client
|
||||
|
||||
}
|
||||
|
||||
func (coll Collection) storage_add(card *Card) error {
|
||||
func (coll Collection) storageAdd(card *Card) error {
|
||||
|
||||
card.SerraUpdated = primitive.NewDateTimeFromTime(time.Now())
|
||||
|
||||
@ -61,7 +70,7 @@ func (coll Collection) storage_add(card *Card) error {
|
||||
|
||||
}
|
||||
|
||||
func (coll Collection) storage_add_set(set *Set) (*mongo.InsertOneResult, error) {
|
||||
func (coll Collection) storageAddSet(set *Set) (*mongo.InsertOneResult, error) {
|
||||
|
||||
id, err := coll.InsertOne(context.TODO(), set)
|
||||
if err != nil {
|
||||
@ -71,7 +80,7 @@ func (coll Collection) storage_add_set(set *Set) (*mongo.InsertOneResult, error)
|
||||
|
||||
}
|
||||
|
||||
func (coll Collection) storage_add_total(p PriceEntry) error {
|
||||
func (coll Collection) storageAddTotal(p PriceEntry) error {
|
||||
|
||||
// create total object if not exists...
|
||||
coll.InsertOne(context.TODO(), Total{ID: "1", Value: []PriceEntry{}})
|
||||
@ -92,91 +101,89 @@ func (coll Collection) storage_add_total(p PriceEntry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (coll Collection) storage_find(filter, sort bson.D) ([]Card, error) {
|
||||
|
||||
opts := options.Find().SetSort(sort)
|
||||
func (coll Collection) storageFind(filter, sort bson.D, skip, limit int64) ([]Card, error) {
|
||||
opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit)
|
||||
cursor, err := coll.Find(context.TODO(), filter, opts)
|
||||
l := Logger()
|
||||
|
||||
if err != nil {
|
||||
LogMessage("Could not query data due to connection errors to database", "red")
|
||||
os.Exit(1)
|
||||
l.Fatalf("Could not query data due to connection errors to database: %s", err.Error())
|
||||
}
|
||||
|
||||
var results []Card
|
||||
if err = cursor.All(context.TODO(), &results); err != nil {
|
||||
log.Fatal(err)
|
||||
l.Fatal(err)
|
||||
return []Card{}, err
|
||||
}
|
||||
return results, nil
|
||||
|
||||
}
|
||||
|
||||
func (coll Collection) storage_find_set(filter, sort bson.D) ([]Set, error) {
|
||||
|
||||
func (coll Collection) storageFindSet(filter, sort bson.D) ([]Set, error) {
|
||||
l := Logger()
|
||||
opts := options.Find().SetSort(sort)
|
||||
|
||||
cursor, err := coll.Find(context.TODO(), filter, opts)
|
||||
if err != nil {
|
||||
LogMessage("Could not query set data due to connection errors to database", "red")
|
||||
os.Exit(1)
|
||||
l.Fatalf("Could not query set data due to connection errors to database: %s", err.Error())
|
||||
}
|
||||
|
||||
var results []Set
|
||||
if err = cursor.All(context.TODO(), &results); err != nil {
|
||||
log.Fatal(err)
|
||||
l.Fatal(err)
|
||||
return []Set{}, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
|
||||
}
|
||||
|
||||
func (coll Collection) storage_find_total() (Total, error) {
|
||||
|
||||
func (coll Collection) storageFindTotal() (Total, error) {
|
||||
var total Total
|
||||
l := Logger()
|
||||
|
||||
err := coll.FindOne(context.TODO(), bson.D{{"_id", "1"}}).Decode(&total)
|
||||
|
||||
if err != nil {
|
||||
LogMessage("Could not query total data due to connection errors to database", "red")
|
||||
os.Exit(1)
|
||||
l.Fatalf("Could not query total data due to connection errors to database: %s", err.Error())
|
||||
}
|
||||
return total, nil
|
||||
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (coll Collection) storage_remove(filter bson.M) error {
|
||||
func (coll Collection) storageRemove(filter bson.M) error {
|
||||
l := Logger()
|
||||
|
||||
_, err := coll.DeleteOne(context.TODO(), filter)
|
||||
if err != nil {
|
||||
LogMessage("Could remove card data due to connection errors to database", "red")
|
||||
os.Exit(1)
|
||||
l.Fatalf("Could remove card data due to connection errors to database: %s", err.Error())
|
||||
}
|
||||
return nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (coll Collection) storage_aggregate(pipeline mongo.Pipeline) ([]primitive.M, error) {
|
||||
|
||||
func (coll Collection) storageAggregate(pipeline mongo.Pipeline) ([]primitive.M, error) {
|
||||
l := Logger()
|
||||
opts := options.Aggregate()
|
||||
|
||||
cursor, err := coll.Aggregate(
|
||||
context.TODO(),
|
||||
pipeline,
|
||||
opts)
|
||||
if err != nil {
|
||||
LogMessage(fmt.Sprintf("%v", err), "red")
|
||||
LogMessage("Could not aggregate data due to connection errors to database", "red")
|
||||
os.Exit(1)
|
||||
l.Fatalf("Could not aggregate data due to connection errors to database: %s", err.Error())
|
||||
}
|
||||
|
||||
// Get a list of all returned documents and print them out.
|
||||
// See the mongo.Cursor documentation for more examples of using cursors.
|
||||
var results []bson.M
|
||||
if err = cursor.All(context.TODO(), &results); err != nil {
|
||||
log.Fatal(err)
|
||||
l.Fatal(err)
|
||||
}
|
||||
return results, nil
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (coll Collection) storage_update(filter, update bson.M) error {
|
||||
|
||||
func (coll Collection) storageUpdate(filter, update bson.M) error {
|
||||
l := Logger()
|
||||
// Call the driver's UpdateOne() method and pass filter and update to it
|
||||
_, err := coll.UpdateOne(
|
||||
context.Background(),
|
||||
@ -184,14 +191,13 @@ func (coll Collection) storage_update(filter, update bson.M) error {
|
||||
update,
|
||||
)
|
||||
if err != nil {
|
||||
LogMessage("Could not update data due to connection errors to database", "red")
|
||||
os.Exit(1)
|
||||
l.Fatalf("Could not update data due to connection errors to database: %s", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func storage_disconnect(client *mongo.Client) error {
|
||||
func storageDisconnect(client *mongo.Client) error {
|
||||
if err := client.Disconnect(context.TODO()); err != nil {
|
||||
return err
|
||||
}
|
||||
23
pkg/serra/undeck.go
Normal file
23
pkg/serra/undeck.go
Normal file
@ -0,0 +1,23 @@
|
||||
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
|
||||
},
|
||||
}
|
||||
@ -19,24 +19,25 @@ func init() {
|
||||
var updateCmd = &cobra.Command{
|
||||
Aliases: []string{"u"},
|
||||
Use: "update",
|
||||
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.`,
|
||||
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.`,
|
||||
SilenceErrors: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
client := storage_connect()
|
||||
defer storage_disconnect(client)
|
||||
client := storageConnect()
|
||||
l := Logger()
|
||||
defer storageDisconnect(client)
|
||||
|
||||
// update sets
|
||||
setscoll := &Collection{client.Database("serra").Collection("sets")}
|
||||
coll := &Collection{client.Database("serra").Collection("cards")}
|
||||
totalcoll := &Collection{client.Database("serra").Collection("total")}
|
||||
|
||||
// predefine query for set analysis. used for total stats later
|
||||
projectStage := bson.D{{"$project",
|
||||
bson.D{
|
||||
{"serra_count", true},
|
||||
{"serra_count_foil", true},
|
||||
{"serra_count_etched", true},
|
||||
{"set", true},
|
||||
{"last_price", bson.D{{"$arrayElemAt", bson.A{"$serra_prices", -1}}}}}}}
|
||||
groupStage := bson.D{
|
||||
@ -45,19 +46,42 @@ var updateCmd = &cobra.Command{
|
||||
{"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"}}}}}},
|
||||
{"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"}}}}}},
|
||||
}}}
|
||||
|
||||
sets, _ := fetch_sets()
|
||||
l.Info("Fetching bulk data from scryfall...")
|
||||
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 {
|
||||
|
||||
// When downloading new sets, PriceList needs to be initialized
|
||||
// This query silently fails if set was already downloaded. Not nice but ok for now.
|
||||
// TODO: make this not fail silently
|
||||
set.SerraPrices = []PriceEntry{}
|
||||
setscoll.storage_add_set(&set)
|
||||
setscoll.storageAddSet(&set)
|
||||
|
||||
cards, _ := coll.storage_find(bson.D{{"set", set.Code}}, bson.D{{"_id", 1}})
|
||||
cards, _ := coll.storageFind(bson.D{{"set", set.Code}}, bson.D{{"_id", 1}}, 0, 0)
|
||||
|
||||
// if no cards in collection for this set, skip it
|
||||
if len(cards) == 0 {
|
||||
@ -74,24 +98,25 @@ var updateCmd = &cobra.Command{
|
||||
SaucerHead: "[green]>[reset]",
|
||||
SaucerPadding: " ",
|
||||
BarStart: "|",
|
||||
BarEnd: fmt.Sprintf("| %s%s%s", Pink, set.Name, Reset),
|
||||
BarEnd: "| " + set.Name,
|
||||
}),
|
||||
)
|
||||
|
||||
for _, card := range cards {
|
||||
bar.Add(1)
|
||||
updated_card, err := fetch_card(fmt.Sprintf("%s/%s", card.Set, card.CollectorNumber))
|
||||
updatedCard, err := getCardFromBulk(updatedCards, card.Set, card.CollectorNumber)
|
||||
if err != nil {
|
||||
LogMessage(fmt.Sprintf("%v", err), "red")
|
||||
l.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
updated_card.Prices.Date = primitive.NewDateTimeFromTime(time.Now())
|
||||
updatedCard.Prices.Date = primitive.NewDateTimeFromTime(time.Now())
|
||||
|
||||
update := bson.M{
|
||||
"$set": bson.M{"serra_updated": primitive.NewDateTimeFromTime(time.Now()), "prices": updated_card.Prices, "collectornumber": updated_card.CollectorNumber},
|
||||
"$push": bson.M{"serra_prices": updated_card.Prices},
|
||||
"$set": bson.M{"serra_updated": primitive.NewDateTimeFromTime(time.Now()), "prices": updatedCard.Prices, "cmc": updatedCard.Cmc, "cardmarketid": updatedCard.CardmarketID, "tcgplayerid": updatedCard.TCGPlayerID},
|
||||
"$push": bson.M{"serra_prices": updatedCard.Prices},
|
||||
}
|
||||
coll.storage_update(bson.M{"_id": bson.M{"$eq": card.ID}}, update)
|
||||
coll.storageUpdate(bson.M{"_id": bson.M{"$eq": card.ID}}, update)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
@ -99,10 +124,10 @@ var updateCmd = &cobra.Command{
|
||||
|
||||
// calculate value summary
|
||||
matchStage := bson.D{{"$match", bson.D{{"set", set.Code}}}}
|
||||
setvalue, _ := coll.storage_aggregate(mongo.Pipeline{matchStage, projectStage, groupStage})
|
||||
setValue, _ := coll.storageAggregate(mongo.Pipeline{matchStage, projectStage, groupStage})
|
||||
|
||||
p := PriceEntry{}
|
||||
s := setvalue[0]
|
||||
s := setValue[0]
|
||||
|
||||
p.Date = primitive.NewDateTimeFromTime(time.Now())
|
||||
|
||||
@ -110,27 +135,26 @@ var updateCmd = &cobra.Command{
|
||||
mapstructure.Decode(s, &p)
|
||||
|
||||
// do the update
|
||||
set_update := bson.M{
|
||||
"$set": bson.M{"serra_updated": p.Date},
|
||||
setUpdate := bson.M{
|
||||
"$set": bson.M{"serra_updated": p.Date, "cardcount": set.CardCount},
|
||||
"$push": bson.M{"serra_prices": p},
|
||||
}
|
||||
// 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)
|
||||
setscoll.storageUpdate(bson.M{"code": bson.M{"$eq": set.Code}}, setUpdate)
|
||||
}
|
||||
|
||||
totalvalue, _ := coll.storage_aggregate(mongo.Pipeline{projectStage, groupStage})
|
||||
totalValue, _ := coll.storageAggregate(mongo.Pipeline{projectStage, groupStage})
|
||||
|
||||
t := PriceEntry{}
|
||||
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
|
||||
// constructed new priceentry
|
||||
tmpCard := Card{}
|
||||
tmpCard.Prices = t
|
||||
|
||||
fmt.Printf("\n%sUpdating total value of collection to: %s%.02f %s%s\n", Green, Yellow, tmpCard.getValue(), getCurrency(), Reset)
|
||||
totalcoll.storage_add_total(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)
|
||||
totalcoll.storageAddTotal(t)
|
||||
|
||||
return nil
|
||||
},
|
||||
121
pkg/serra/web.go
Normal file
121
pkg/serra/web.go
Normal file
@ -0,0 +1,121 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
73
readme.md
73
readme.md
@ -22,11 +22,12 @@ an overview in what cards you own and what value they have.
|
||||
|
||||
**What Serra does not**
|
||||
|
||||
* Does not give a shit about conditions (NM, M, GD...)
|
||||
* Does not care about conditions (NM, M, GD...)
|
||||
* Does not track etched cards. Only normal and foil.
|
||||
|
||||
# Quickstart
|
||||
|
||||
### Install Binaries
|
||||
## Install Binaries
|
||||
|
||||
on macOS you can use
|
||||
|
||||
@ -36,16 +37,15 @@ on Linux/BSD/Windows you can download binaries from
|
||||
|
||||
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.
|
||||
|
||||
|
||||
You can also use the docker-compose setup included in this Repo:
|
||||
|
||||
docker-compose up -d
|
||||
|
||||
### Configure the Database
|
||||
## Configure the Database
|
||||
|
||||
Configure `serra` via Environment variables
|
||||
|
||||
@ -69,6 +69,7 @@ Usage:
|
||||
Available Commands:
|
||||
add Add a card to 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
|
||||
flops What cards lost most value
|
||||
help Help about any command
|
||||
@ -137,6 +138,12 @@ update. After all Sets are updated, the whole collection value is updated.
|
||||
|
||||

|
||||
|
||||
## Check
|
||||
|
||||
To add a card to your collection.
|
||||
|
||||

|
||||
|
||||
## Adding all those cards, manually?
|
||||
|
||||
Yes. While there are serveral OCR/Photo Scanners for mtg cards, I found they
|
||||
@ -156,9 +163,65 @@ one> 3
|
||||
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
|
||||
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
|
||||
|
||||
## Install
|
||||
|
||||
15
scripts/export.py
Normal file
15
scripts/export.py
Normal file
@ -0,0 +1,15 @@
|
||||
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"]))
|
||||
19
scripts/removebulk.fish
Executable file
19
scripts/removebulk.fish
Executable file
@ -0,0 +1,19 @@
|
||||
#!/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
|
||||
13
scripts/replace.sh
Executable file
13
scripts/replace.sh
Executable file
@ -0,0 +1,13 @@
|
||||
#!/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
|
||||
113
src/serra/add.go
113
src/serra/add.go
@ -1,113 +0,0 @@
|
||||
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)")
|
||||
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(), getCurrency()), "red")
|
||||
continue
|
||||
}
|
||||
|
||||
modify_count_of_card(coll, &c, count)
|
||||
// Give feedback of successfully added card
|
||||
LogMessage(fmt.Sprintf("%dx \"%s\" (%s, %.2f %s) added to Collection.", c.SerraCount, c.Name, c.Rarity, c.getValue(), 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
|
||||
c.SerraCount = count
|
||||
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.", c.SerraCount, c.Name, c.Rarity, c.getValue(), getCurrency()), "green")
|
||||
}
|
||||
}
|
||||
storage_disconnect(client)
|
||||
return nil
|
||||
}
|
||||
@ -1,149 +0,0 @@
|
||||
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, Purple, card.Name, Reset, card.Set, card.CollectorNumber, Yellow, card.getValue(), getCurrency(), Reset), "normal")
|
||||
total = total + card.getValue()*float64(card.SerraCount)
|
||||
}
|
||||
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: %dx\n", card.SerraCount)
|
||||
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(), getCurrency(), Reset)
|
||||
|
||||
fmt.Printf("\n%sHistory%s\n", Green, Reset)
|
||||
print_price_history(card.SerraPrices, "* ")
|
||||
fmt.Println()
|
||||
return nil
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
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"
|
||||
}
|
||||
@ -1,182 +0,0 @@
|
||||
package serra
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"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) 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}}
|
||||
update := bson.M{
|
||||
"$set": bson.M{"serra_count": stored_card.SerraCount + amount},
|
||||
}
|
||||
coll.storage_update(update_filter, update)
|
||||
|
||||
LogMessage(fmt.Sprintf("Updating Card \"%s\" amount to %d", stored_card.Name, stored_card.SerraCount+amount), "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) {
|
||||
|
||||
var before float64
|
||||
for _, e := range prices {
|
||||
|
||||
// TODO: Make currency configurable
|
||||
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
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
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(), getCurrency(), ncard.Name, ncard.SetName)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
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 since string
|
||||
var sortby string
|
||||
var cardType string
|
||||
var unique 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)
|
||||
}
|
||||
}
|
||||
@ -1,111 +0,0 @@
|
||||
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(), "$serra_count"}}}}}},
|
||||
{"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}}},
|
||||
}}},
|
||||
})
|
||||
|
||||
fmt.Printf("\n%sCards %s\n", Green, Reset)
|
||||
fmt.Printf("Total: %s%.0f%s\n", Yellow, stats[0]["count"], Reset)
|
||||
fmt.Printf("Unique: %s%d%s\n", Purple, stats[0]["unique"], 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)
|
||||
fmt.Printf("Current: %s%.2f %s%s\n", Pink, stats[0]["value"], getCurrency(), Reset)
|
||||
total, _ := totalcoll.storage_find_total()
|
||||
|
||||
fmt.Printf("History: \n")
|
||||
print_price_history(total.Value, "* ")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -4,9 +4,10 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Serra</title>
|
||||
<!-- <link rel="stylesheet" href="https://unpkg.com/bulma-dracula"> -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
||||
<title>{{.title}}{{ if .query.Set }} - Set: {{.query.Set}}{{end}}</title>
|
||||
<link rel="stylesheet" href="https://jenil.github.io/bulmaswatch/cosmo/bulmaswatch.min.css">
|
||||
<!-- <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"> -->
|
||||
|
||||
<style>
|
||||
.cardpreview {
|
||||
@ -63,10 +64,10 @@
|
||||
<body>
|
||||
|
||||
<!-- Site Title -->
|
||||
<section class="hero is-primary">
|
||||
<section class="hero is-black">
|
||||
<div class="hero-body">
|
||||
<p class="title">
|
||||
{{ .title }}
|
||||
<a href="/">{{ .title }}</a>
|
||||
</p>
|
||||
<p class="subtitle">
|
||||
<i>Magic: The Gathering</i> Collection
|
||||
@ -84,7 +85,7 @@
|
||||
<div class="field">
|
||||
<label class="label">Set</label>
|
||||
<div class="control">
|
||||
<div class="select is-primary">
|
||||
<div class="select">
|
||||
<select name="set" id="set" form="searchform">
|
||||
<option value="">-</option>
|
||||
{{range .sets}}
|
||||
@ -106,11 +107,14 @@
|
||||
</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="field">
|
||||
<label class="label">Sort</label>
|
||||
<div class="control">
|
||||
<div class="select is-primary">
|
||||
<div class="select">
|
||||
<select name="sort" id="sort" form="searchform">
|
||||
<option value="name" selected>Name</option>
|
||||
<option value="value">Value</option>
|
||||
@ -164,7 +168,7 @@
|
||||
<tbody>
|
||||
{{range .cards}}
|
||||
<tr>
|
||||
<td>{{.SerraCount}}</td>
|
||||
<td>{{ add .SerraCount .SerraCountFoil }}</td>
|
||||
<td>
|
||||
<div class="cardpreview"><strong>{{.Name }}</strong>
|
||||
<span class="cardpreviewtext">
|
||||
@ -184,19 +188,74 @@
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</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">…</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">…</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 -->
|
||||
<script>
|
||||
function getParam(paramName) {
|
||||
return decodeURI(
|
||||
(RegExp(paramName + '=' + '(.+?)(&|$)').exec(location.search) || [, null])[1]
|
||||
(RegExp(paramName + '=' + '(.+?)(&|$)').exec(location.search) || [, 500])[1]
|
||||
);
|
||||
}
|
||||
var selectedSetVal = getParam("set");
|
||||
document.getElementById("set").value = selectedSetVal;
|
||||
|
||||
var selectedLimitVal = getParam("limit");
|
||||
document.getElementById("limit").value = selectedLimitVal;
|
||||
|
||||
var selectedSortVal = getParam("sort");
|
||||
document.getElementById("sort").value = selectedSortVal;
|
||||
</script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user