Compare commits
No commits in common. "main" and "audiolion-audiolion/support-foils" have entirely different histories.
main
...
audiolion-
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,6 +1,7 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
serra
|
||||||
|
!src/serra
|
||||||
_db/*
|
_db/*
|
||||||
/serra
|
|
||||||
!_db/.placeholder
|
!_db/.placeholder
|
||||||
_backup/*
|
_backup/*
|
||||||
!_backup/.placeholder
|
!_backup/.placeholder
|
||||||
|
|||||||
@ -1,51 +0,0 @@
|
|||||||
version: 2
|
|
||||||
before:
|
|
||||||
hooks:
|
|
||||||
- go mod tidy
|
|
||||||
- go generate ./...
|
|
||||||
builds:
|
|
||||||
- env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
goos:
|
|
||||||
- linux
|
|
||||||
- windows
|
|
||||||
- darwin
|
|
||||||
- openbsd
|
|
||||||
ldflags:
|
|
||||||
- -X github.com/noqqe/serra/pkg/serra.Version={{.Tag}}
|
|
||||||
flags:
|
|
||||||
- -v
|
|
||||||
main: ./cmd/serra
|
|
||||||
archives:
|
|
||||||
- id: serra
|
|
||||||
name_template: >-
|
|
||||||
{{ .ProjectName }}_
|
|
||||||
{{- title .Os }}_
|
|
||||||
{{- if eq .Arch "amd64" }}x86_64
|
|
||||||
{{- else if eq .Arch "386" }}i386
|
|
||||||
{{- else }}{{ .Arch }}{{ end }}
|
|
||||||
changelog:
|
|
||||||
sort: asc
|
|
||||||
filters:
|
|
||||||
exclude:
|
|
||||||
- '^docs:'
|
|
||||||
- '^test:'
|
|
||||||
brews:
|
|
||||||
- name: serra
|
|
||||||
goarm: 6
|
|
||||||
repository:
|
|
||||||
owner: noqqe
|
|
||||||
name: homebrew-tap
|
|
||||||
download_strategy: CurlDownloadStrategy
|
|
||||||
directory: Formula
|
|
||||||
commit_author:
|
|
||||||
name: goreleaserbot
|
|
||||||
email: goreleaser@carlosbecker.com
|
|
||||||
homepage: "https://github.com/noqqe/serra"
|
|
||||||
description: "serra - Personal Magic: The Gathering Collection Tracker "
|
|
||||||
license: "MIT"
|
|
||||||
test: |
|
|
||||||
system "#{bin}/serra --version"
|
|
||||||
install: |
|
|
||||||
bin.install "serra"
|
|
||||||
|
|
||||||
97
.goreleaser.yml
Normal file
97
.goreleaser.yml
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# This is an example .goreleaser.yml file with some sane defaults.
|
||||||
|
# Make sure to check the documentation at http://goreleaser.com
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
# You may remove this if you don't use go modules.
|
||||||
|
- go mod tidy
|
||||||
|
# you may remove this if you don't need go generate
|
||||||
|
- go generate ./...
|
||||||
|
builds:
|
||||||
|
- env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- windows
|
||||||
|
- darwin
|
||||||
|
- openbsd
|
||||||
|
ldflags:
|
||||||
|
- -X github.com/noqqe/serra/src/serra.Version={{.Tag}}
|
||||||
|
flags:
|
||||||
|
- -v
|
||||||
|
archives:
|
||||||
|
- id: serra
|
||||||
|
name_template: >-
|
||||||
|
{{ .ProjectName }}_
|
||||||
|
{{- title .Os }}_
|
||||||
|
{{- if eq .Arch "amd64" }}x86_64
|
||||||
|
{{- else if eq .Arch "386" }}i386
|
||||||
|
{{- else }}{{ .Arch }}{{ end }}
|
||||||
|
checksum:
|
||||||
|
name_template: 'checksums.txt'
|
||||||
|
snapshot:
|
||||||
|
name_template: "{{ .Tag }}-next"
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- '^docs:'
|
||||||
|
- '^test:'
|
||||||
|
brews:
|
||||||
|
-
|
||||||
|
# Name template of the recipe
|
||||||
|
# Default to project name
|
||||||
|
name: serra
|
||||||
|
|
||||||
|
# GOARM to specify which 32-bit arm version to use if there are multiple versions
|
||||||
|
# from the build section. Brew formulas support atm only one 32-bit version.
|
||||||
|
# Default is 6 for all artifacts or each id if there a multiple versions.
|
||||||
|
goarm: 6
|
||||||
|
|
||||||
|
# NOTE: make sure the url_template, the token and given repo (github or gitlab) owner and name are from the
|
||||||
|
# same kind. We will probably unify this in the next major version like it is done with scoop.
|
||||||
|
|
||||||
|
# GitHub/GitLab repository to push the formula to
|
||||||
|
# Gitea is not supported yet, but the support coming
|
||||||
|
tap:
|
||||||
|
owner: noqqe
|
||||||
|
name: homebrew-tap
|
||||||
|
# Optionally a token can be provided, if it differs from the token provided to GoReleaser
|
||||||
|
|
||||||
|
# Allows you to set a custom download strategy. Note that you'll need
|
||||||
|
# to implement the strategy and add it to your tap repository.
|
||||||
|
# Example: https://docs.brew.sh/Formula-Cookbook#specifying-the-download-strategy-explicitly
|
||||||
|
# Default is empty.
|
||||||
|
download_strategy: CurlDownloadStrategy
|
||||||
|
|
||||||
|
# Git author used to commit to the repository.
|
||||||
|
# Defaults are shown.
|
||||||
|
commit_author:
|
||||||
|
name: goreleaserbot
|
||||||
|
email: goreleaser@carlosbecker.com
|
||||||
|
|
||||||
|
# Folder inside the repository to put the formula.
|
||||||
|
# Default is the root folder.
|
||||||
|
folder: Formula
|
||||||
|
|
||||||
|
# Your app's homepage.
|
||||||
|
# Default is empty.
|
||||||
|
homepage: "https://github.com/noqqe/serra"
|
||||||
|
|
||||||
|
# Your app's description.
|
||||||
|
# Default is empty.
|
||||||
|
description: "serra - Personal Magic: The Gathering Collection Tracker "
|
||||||
|
|
||||||
|
# SPDX identifier of your app's license.
|
||||||
|
# Default is empty.
|
||||||
|
license: "MIT"
|
||||||
|
|
||||||
|
# So you can `brew test` your formula.
|
||||||
|
# Default is empty.
|
||||||
|
test: |
|
||||||
|
system "#{bin}/serra --version"
|
||||||
|
|
||||||
|
# Custom install script for brew.
|
||||||
|
# Default is 'bin.install "program"'.
|
||||||
|
install: |
|
||||||
|
bin.install "serra"
|
||||||
|
|
||||||
@ -1 +1 @@
|
|||||||
golang 1.21.3
|
golang 1.20
|
||||||
|
|||||||
15
Dockerfile
15
Dockerfile
@ -1,24 +1,23 @@
|
|||||||
FROM golang:1.25-alpine AS build
|
FROM golang:alpine AS builder
|
||||||
|
|
||||||
RUN apk update && apk add --no-cache git ca-certificates curl
|
RUN apk update && apk add --no-cache git
|
||||||
|
|
||||||
WORKDIR /go/src/app
|
WORKDIR /go/src/app
|
||||||
COPY pkg /go/src/app/pkg
|
COPY src /go/src/app/src
|
||||||
COPY cmd /go/src/app/cmd
|
|
||||||
COPY templates /go/src/app/templates
|
COPY templates /go/src/app/templates
|
||||||
COPY go.mod /go/src/app/go.mod
|
COPY go.mod /go/src/app/go.mod
|
||||||
COPY go.sum /go/src/app/go.sum
|
COPY go.sum /go/src/app/go.sum
|
||||||
COPY .git /go/src/app/.git
|
COPY .git /go/src/app/.git
|
||||||
|
COPY serra.go /go/src/app/serra.go
|
||||||
|
|
||||||
# build
|
# build
|
||||||
RUN go build -ldflags "-X github.com/noqqe/serra/pkg/serra.Version=`git describe --tags`" -v cmd/serra/serra.go
|
RUN go get -v ./...
|
||||||
|
RUN go build -ldflags "-X github.com/noqqe/serra/src/serra.Version=`git describe --tags`" -v serra.go
|
||||||
|
|
||||||
# copy
|
# copy
|
||||||
FROM scratch
|
FROM scratch
|
||||||
WORKDIR /go/src/app
|
WORKDIR /go/src/app
|
||||||
COPY --from=build /go/src/app/serra /go/src/app/serra
|
COPY --from=builder /go/src/app/serra /go/src/app/serra
|
||||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
|
||||||
COPY --from=build /tmp /tmp
|
|
||||||
COPY templates /go/src/app/templates
|
COPY templates /go/src/app/templates
|
||||||
|
|
||||||
# run
|
# run
|
||||||
|
|||||||
21
LICENSE
21
LICENSE
@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2023 Florian Baumann
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@ -5,17 +5,17 @@ version: '3'
|
|||||||
tasks:
|
tasks:
|
||||||
build:
|
build:
|
||||||
cmds:
|
cmds:
|
||||||
- go build -ldflags "-X github.com/noqqe/serra/pkg/serra.Version=`git describe --tags`" -v cmd/serra/serra.go
|
- go build -ldflags "-X github.com/noqqe/serra/src/serra.Version=`git describe --tags`" -v serra.go
|
||||||
sources:
|
sources:
|
||||||
- "pkg/serra/**/*.go"
|
- "src/serra/**/*.go"
|
||||||
- "cmd/serra/serra.go"
|
- "serra.go"
|
||||||
generates:
|
generates:
|
||||||
- "./serra"
|
- "./serra"
|
||||||
|
|
||||||
release:
|
release:
|
||||||
interactive: true
|
interactive: true
|
||||||
cmds:
|
cmds:
|
||||||
- git tag | sort -t. -k 1,1n -k 2,2n -k 3,3n | tail -5
|
- git tag | tail -5
|
||||||
- read -p "Version v1.1.1 " version ; git tag $version
|
- read -p "Version v1.1.1 " version ; git tag $version
|
||||||
- git push --tags
|
- git push --tags
|
||||||
- goreleaser release --clean
|
- goreleaser release --clean
|
||||||
|
|||||||
@ -2,7 +2,7 @@ version: '3.6'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
mongo:
|
mongo:
|
||||||
image: mongo:6
|
image: mongo
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 27017:27017
|
- 27017:27017
|
||||||
|
|||||||
72
go.mod
72
go.mod
@ -1,67 +1,23 @@
|
|||||||
module github.com/noqqe/serra
|
module github.com/noqqe/serra
|
||||||
|
|
||||||
go 1.25.0
|
go 1.14
|
||||||
|
|
||||||
toolchain go1.25.4
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/charmbracelet/log v0.4.0
|
|
||||||
github.com/chzyer/readline v1.5.1
|
github.com/chzyer/readline v1.5.1
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.9.0
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
|
||||||
github.com/schollz/progressbar/v3 v3.16.1
|
|
||||||
github.com/spf13/cobra v1.8.1
|
|
||||||
go.mongodb.org/mongo-driver v1.17.1
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
|
||||||
github.com/bytedance/sonic v1.12.3 // indirect
|
|
||||||
github.com/bytedance/sonic/loader v0.2.1 // indirect
|
|
||||||
github.com/charmbracelet/lipgloss v0.13.0 // indirect
|
|
||||||
github.com/charmbracelet/x/ansi v0.3.2 // indirect
|
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
|
||||||
github.com/dolmen-go/kittyimg v0.0.0-20250610224728-874967bd8ea4 // indirect
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
|
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
|
||||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
|
||||||
github.com/go-playground/validator/v10 v10.22.1 // indirect
|
|
||||||
github.com/goccy/go-json v0.10.3 // indirect
|
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
|
github.com/google/go-cmp v0.5.6 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/klauspost/compress v1.15.15 // indirect
|
||||||
github.com/klauspost/compress v1.17.11 // indirect
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
github.com/montanaflynn/stats v0.7.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/rivo/uniseg v0.4.4 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/schollz/progressbar/v3 v3.13.0
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/spf13/cobra v1.6.1
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
|
||||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
|
||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
|
||||||
github.com/muesli/termenv v0.15.2 // indirect
|
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
|
||||||
github.com/xdg-go/scram v1.1.2 // indirect
|
github.com/xdg-go/scram v1.1.2 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
go.mongodb.org/mongo-driver v1.11.3
|
||||||
golang.org/x/arch v0.11.0 // indirect
|
golang.org/x/crypto v0.6.0 // indirect
|
||||||
golang.org/x/crypto v0.28.0 // indirect
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
golang.org/x/net v0.30.0 // indirect
|
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
|
||||||
golang.org/x/term v0.29.0 // indirect
|
|
||||||
golang.org/x/text v0.19.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.35.1 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
207
go.sum
207
go.sum
@ -1,180 +1,195 @@
|
|||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
|
||||||
github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
|
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||||
github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||||
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
|
||||||
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
|
|
||||||
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
|
|
||||||
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
|
|
||||||
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
|
|
||||||
github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY=
|
|
||||||
github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
|
||||||
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
|
|
||||||
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
|
|
||||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
||||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dolmen-go/kittyimg v0.0.0-20250610224728-874967bd8ea4 h1:KGRb+vxMx5pGsfDjDSW2Th+b2OEflb0yC3s0daCmiYU=
|
|
||||||
github.com/dolmen-go/kittyimg v0.0.0-20250610224728-874967bd8ea4/go.mod h1:2vk7ATPVcI7uW4Sh6PrSQvtO+Czmq8509xcg/y8Osd0=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
|
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
|
||||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
|
||||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
|
||||||
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
|
||||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
|
||||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||||
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||||
|
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
|
||||||
|
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||||
|
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU=
|
||||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||||
|
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/schollz/progressbar/v3 v3.16.1 h1:RnF1neWZFzLCoGx8yp1yF7SDl4AzNDI5y4I0aUJRrZQ=
|
github.com/schollz/progressbar/v3 v3.13.0 h1:9TeeWRcjW2qd05I8Kf9knPkW4vLM/hYoa6z9ABvxje8=
|
||||||
github.com/schollz/progressbar/v3 v3.16.1/go.mod h1:I2ILR76gz5VXqYMIY/LdLecvMHDPVcQm3W/MSKi1TME=
|
github.com/schollz/progressbar/v3 v3.13.0/go.mod h1:ZBYnSuLAX2LU8P8UiKN/KgF2DY58AJC8yfVYLPC8Ly4=
|
||||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
|
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||||
|
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM=
|
go.mongodb.org/mongo-driver v1.11.3 h1:Ql6K6qYHEzB6xvu4+AU0BoRoqf9vFPcc4o7MUIdPW8Y=
|
||||||
go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
|
go.mongodb.org/mongo-driver v1.11.3/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
|
||||||
golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||||
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
|
||||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||||
|
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
|||||||
BIN
imgs/check.png
BIN
imgs/check.png
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB |
190
pkg/serra/add.go
190
pkg/serra/add.go
@ -1,190 +0,0 @@
|
|||||||
package serra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/chzyer/readline"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
addCmd.Flags().Int64VarP(&count, "count", "c", 1, "Amount of cards to add")
|
|
||||||
addCmd.Flags().BoolVarP(&unique, "unique", "u", false, "Only add card if not existent yet")
|
|
||||||
addCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Spin up interactive terminal")
|
|
||||||
addCmd.Flags().StringVarP(&set, "set", "s", "", "Filter by set code (usg/mmq/vow)")
|
|
||||||
addCmd.Flags().BoolVarP(&foil, "foil", "f", false, "Add foil variant of card")
|
|
||||||
rootCmd.AddCommand(addCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
var addCmd = &cobra.Command{
|
|
||||||
Aliases: []string{"a"},
|
|
||||||
Use: "add",
|
|
||||||
Short: "Add a card to your collection",
|
|
||||||
Long: "Adds a card from scryfall to your collection. Amount can be modified using flags",
|
|
||||||
SilenceErrors: true,
|
|
||||||
RunE: func(cmd *cobra.Command, cards []string) error {
|
|
||||||
if interactive {
|
|
||||||
addCardsInteractive(unique, set)
|
|
||||||
} else {
|
|
||||||
addCards(cards, unique, count)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func addCardsInteractive(unique bool, set string) {
|
|
||||||
l := Logger()
|
|
||||||
if len(set) == 0 {
|
|
||||||
l.Fatal("Option --set <set> must be given in interactive mode")
|
|
||||||
}
|
|
||||||
|
|
||||||
rl, err := readline.New(fmt.Sprintf("%s> ", set))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer rl.Close()
|
|
||||||
|
|
||||||
for {
|
|
||||||
line, err := rl.Readline()
|
|
||||||
if err != nil { // io.EOF
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// default is no foil
|
|
||||||
foil = false
|
|
||||||
|
|
||||||
// default is count 1
|
|
||||||
count = 1
|
|
||||||
|
|
||||||
// construct card input for addCards
|
|
||||||
card := []string{}
|
|
||||||
|
|
||||||
// Detect if input contains a dash, if it does it means the user wants to add a range of cards
|
|
||||||
if strings.Contains(line, "-") {
|
|
||||||
// Split input into two parts
|
|
||||||
parts := strings.Split(line, "-")
|
|
||||||
// Check if both parts are numbers
|
|
||||||
if _, err := strconv.Atoi(parts[0]); err == nil {
|
|
||||||
if _, err = strconv.Atoi(parts[1]); err == nil {
|
|
||||||
// Loop over range and add each card to card slice
|
|
||||||
start, _ := strconv.Atoi(parts[0])
|
|
||||||
end, _ := strconv.Atoi(parts[1])
|
|
||||||
for i := start; i <= end; i++ {
|
|
||||||
card = append(card, fmt.Sprintf("%s/%d", set, i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
card = append(card, fmt.Sprintf("%s/%s", set, strings.Split(line, " ")[0]))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Are there extra arguments?
|
|
||||||
if len(strings.Split(line, " ")) == 2 {
|
|
||||||
|
|
||||||
// foil shortcut
|
|
||||||
if strings.Split(line, " ")[1] == "f" {
|
|
||||||
foil = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// amount shortcut
|
|
||||||
if amount, err := strconv.Atoi(strings.Split(line, " ")[1]); err == nil {
|
|
||||||
if amount > 1 {
|
|
||||||
count = int64(amount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addCards(card, unique, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func addCards(cards []string, unique bool, count int64) error {
|
|
||||||
client := storageConnect()
|
|
||||||
coll := &Collection{client.Database("serra").Collection("cards")}
|
|
||||||
l := Logger()
|
|
||||||
defer storageDisconnect(client)
|
|
||||||
|
|
||||||
// Loop over different cards
|
|
||||||
for _, card := range cards {
|
|
||||||
// Extract collector number and set name from card input & trim any leading 0 from collector number
|
|
||||||
|
|
||||||
if !strings.Contains(card, "/") {
|
|
||||||
l.Errorf("Invalid card format %s. Needs to be set/collector number i.e. \"usg/13\"", card)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
setName := strings.ToLower(strings.Split(card, "/")[0])
|
|
||||||
collectorNumber := strings.TrimLeft(strings.Split(card, "/")[1], "0")
|
|
||||||
|
|
||||||
if collectorNumber == "" {
|
|
||||||
l.Errorf("Invalid card format %s. Needs to be set/collector number i.e. \"usg/13\"", card)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if card is already in collection
|
|
||||||
co, err := coll.storageFind(bson.D{{"set", setName}, {"collectornumber", collectorNumber}}, bson.D{}, 0, 0)
|
|
||||||
if err != nil {
|
|
||||||
l.Error(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(co) >= 1 {
|
|
||||||
c := co[0]
|
|
||||||
outputColor := coloredValue(c.getValue(foil))
|
|
||||||
|
|
||||||
if unique {
|
|
||||||
l.Warnf("%dx \"%s\" (%s, %s%.2f%s%s) not added, because it already exists", count, c.Name, c.Rarity, outputColor, c.getValue(foil), getCurrency(), Reset)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if askConfirmation(&c) {
|
|
||||||
modifyCardCount(coll, &c, count, foil)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Fetch card from scryfall
|
|
||||||
c, err := fetchCard(setName, collectorNumber)
|
|
||||||
|
|
||||||
outputColor := coloredValue(c.getValue(foil))
|
|
||||||
if err != nil {
|
|
||||||
l.Warn(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write card to mongodb
|
|
||||||
var total int64 = 0
|
|
||||||
if foil {
|
|
||||||
c.SerraCountFoil = count
|
|
||||||
c.SerraCountFoilDeck = 0
|
|
||||||
total = c.SerraCountFoil
|
|
||||||
} else {
|
|
||||||
c.SerraCount = count
|
|
||||||
c.SerraCountDeck = 0
|
|
||||||
total = c.SerraCount
|
|
||||||
}
|
|
||||||
|
|
||||||
if !askConfirmation(c) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err = coll.storageAdd(c)
|
|
||||||
if err != nil {
|
|
||||||
l.Warn(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Give feedback of successfully added card
|
|
||||||
if foil {
|
|
||||||
l.Infof("%dx \"%s\" (%s, %s%.2f%s%s, foil) added", total, c.Name, c.Rarity, outputColor, c.getValue(foil), getCurrency(), Reset)
|
|
||||||
} else {
|
|
||||||
l.Infof("%dx \"%s\" (%s, %s%.2f%s%s) added", total, c.Name, c.Rarity, outputColor, c.getValue(foil), getCurrency(), Reset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
storageDisconnect(client)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,243 +0,0 @@
|
|||||||
package serra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"image/png"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/dolmen-go/kittyimg"
|
|
||||||
"github.com/nfnt/resize"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
cardCmd.Flags().StringVarP(&artist, "artist", "a", "", "Filter by name of artist")
|
|
||||||
cardCmd.Flags().StringVarP(&rarity, "rarity", "r", "", "Filter by rarity of cards (mythic, rare, uncommon, common)")
|
|
||||||
cardCmd.Flags().StringVarP(&set, "set", "e", "", "Filter by set code (usg/mmq/vow)")
|
|
||||||
cardCmd.Flags().StringVarP(&sortby, "sort", "s", "name", "How to sort cards (value/number/name/added)")
|
|
||||||
cardCmd.Flags().StringVarP(&name, "name", "n", "", "Name of the card (regex compatible)")
|
|
||||||
cardCmd.Flags().Int64VarP(&cmc, "cmc", "m", -1, "Cumulative mana cost of card")
|
|
||||||
cardCmd.Flags().StringVarP(&color, "color", "i", "", "Color identity of card (w,u,b,r,g)")
|
|
||||||
cardCmd.Flags().StringVarP(&oracle, "oracle", "o", "", "Contains string in card text")
|
|
||||||
cardCmd.Flags().StringVarP(&cardType, "type", "t", "", "Contains string in card type line")
|
|
||||||
cardCmd.Flags().Int64VarP(&count, "min-count", "c", 0, "Occource more than X in your collection")
|
|
||||||
cardCmd.Flags().BoolVarP(&detail, "detail", "d", false, "Show details for cards (url)")
|
|
||||||
cardCmd.Flags().BoolVarP(&reserved, "reserved", "w", false, "If card is on reserved list")
|
|
||||||
cardCmd.Flags().BoolVarP(&foil, "foil", "f", false, "If card is foil list")
|
|
||||||
cardCmd.Flags().BoolVarP(&drawImg, "image", "g", false, "Draw card image using kitty format")
|
|
||||||
cardCmd.Flags().BoolVarP(&omitInDeck, "omit-in-deck", "q", false, "Omit cards that are in decks")
|
|
||||||
rootCmd.AddCommand(cardCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
var cardCmd = &cobra.Command{
|
|
||||||
Aliases: []string{"cards"},
|
|
||||||
Use: "card [card]",
|
|
||||||
Short: "Search & show cards from your collection",
|
|
||||||
Long: `Search and show cards from your collection.
|
|
||||||
If you directly put a card as an argument, it will be displayed
|
|
||||||
otherwise you'll get a list of cards as a search result.`,
|
|
||||||
SilenceErrors: true,
|
|
||||||
RunE: func(cmd *cobra.Command, cards []string) error {
|
|
||||||
if len(cards) == 0 {
|
|
||||||
cardList := Cards(rarity, set, sortby, name, oracle, cardType, reserved, foil, 0, 0, omitInDeck)
|
|
||||||
showCardList(cardList, detail)
|
|
||||||
} else {
|
|
||||||
ShowCard(cards)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func ShowCard(cardids []string) {
|
|
||||||
client := storageConnect()
|
|
||||||
coll := &Collection{client.Database("serra").Collection("cards")}
|
|
||||||
l := Logger()
|
|
||||||
defer storageDisconnect(client)
|
|
||||||
|
|
||||||
for _, v := range cardids {
|
|
||||||
if len(strings.Split(v, "/")) < 2 || strings.Split(v, "/")[1] == "" {
|
|
||||||
l.Warnf("Invalid card %s", v)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
cards, _ := coll.storageFind(bson.D{{"set", strings.Split(v, "/")[0]}, {"collectornumber", strings.Split(v, "/")[1]}}, bson.D{{"name", 1}}, 0, 0)
|
|
||||||
|
|
||||||
for _, card := range cards {
|
|
||||||
showCardDetails(&card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Cards(rarity, set, sortby, name, oracle, cardType string, reserved, foil bool, skip, limit int64, omitInDeck bool) []Card {
|
|
||||||
client := storageConnect()
|
|
||||||
coll := &Collection{client.Database("serra").Collection("cards")}
|
|
||||||
defer storageDisconnect(client)
|
|
||||||
|
|
||||||
filter := bson.D{}
|
|
||||||
|
|
||||||
switch rarity {
|
|
||||||
case "uncommon":
|
|
||||||
filter = append(filter, bson.E{"rarity", "uncommon"})
|
|
||||||
case "common":
|
|
||||||
filter = append(filter, bson.E{"rarity", "common"})
|
|
||||||
case "rare":
|
|
||||||
filter = append(filter, bson.E{"rarity", "rare"})
|
|
||||||
case "mythic":
|
|
||||||
filter = append(filter, bson.E{"rarity", "mythic"})
|
|
||||||
}
|
|
||||||
|
|
||||||
var sortStage bson.D
|
|
||||||
switch sortby {
|
|
||||||
case "value":
|
|
||||||
if getCurrency() == EUR {
|
|
||||||
sortStage = bson.D{{"prices.eur", 1}}
|
|
||||||
} else {
|
|
||||||
sortStage = bson.D{{"prices.usd", 1}}
|
|
||||||
}
|
|
||||||
case "number":
|
|
||||||
sortStage = bson.D{{"collectornumber", 1}}
|
|
||||||
case "name":
|
|
||||||
sortStage = bson.D{{"name", 1}}
|
|
||||||
case "added":
|
|
||||||
sortStage = bson.D{{"serra_created", 1}}
|
|
||||||
default:
|
|
||||||
sortStage = bson.D{{"name", 1}}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(set) > 0 {
|
|
||||||
filter = append(filter, bson.E{"set", set})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(name) > 0 {
|
|
||||||
filter = append(filter, bson.E{"name", bson.D{{"$regex", ".*" + name + ".*"}, {"$options", "i"}}})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(artist) > 0 {
|
|
||||||
filter = append(filter, bson.E{"artist", bson.D{{"$regex", ".*" + artist + ".*"}, {"$options", "i"}}})
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmc > -1 {
|
|
||||||
filter = append(filter, bson.E{"cmc", cmc})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(oracle) > 0 {
|
|
||||||
filter = append(filter, bson.E{"oracletext", bson.D{{"$regex", ".*" + oracle + ".*"}, {"$options", "i"}}})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cardType) > 0 {
|
|
||||||
filter = append(filter, bson.E{"typeline", bson.D{{"$regex", ".*" + cardType + ".*"}, {"$options", "i"}}})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(color) > 0 {
|
|
||||||
colorArr := strings.Split(strings.ToUpper(color), ",")
|
|
||||||
filter = append(filter, bson.E{"coloridentity", colorArr})
|
|
||||||
}
|
|
||||||
|
|
||||||
if reserved {
|
|
||||||
filter = append(filter, bson.E{"reserved", true})
|
|
||||||
}
|
|
||||||
|
|
||||||
if foil {
|
|
||||||
filter = append(filter, bson.E{"serra_count_foil", bson.D{{"$gt", 0}}})
|
|
||||||
}
|
|
||||||
|
|
||||||
if omitInDeck {
|
|
||||||
filter = append(filter, bson.E{ "$expr", bson.D{{"$or",bson.A{
|
|
||||||
bson.D{{"$ne", bson.A{"$serra_count", "$serra_count_deck"}}},
|
|
||||||
bson.D{{"$ne", bson.A{"$serra_count_foil", "$serra_count_foil_deck"}}},
|
|
||||||
}}}})
|
|
||||||
}
|
|
||||||
|
|
||||||
cards, _ := coll.storageFind(filter, sortStage, skip, limit)
|
|
||||||
|
|
||||||
// This is needed because collectornumbers are strings (ie. "23a") but still we
|
|
||||||
// want it to be sorted numerically ... 1,2,3,10,11,100.
|
|
||||||
if sortby == "number" {
|
|
||||||
sort.Slice(cards, func(i, j int) bool {
|
|
||||||
return filterForDigits(cards[i].CollectorNumber) < filterForDigits(cards[j].CollectorNumber)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter out cards that do not reach the minimum amount (--min-count)
|
|
||||||
// this is done after query result because find query constructed does not support
|
|
||||||
// aggregating fields (of count and countFoil).
|
|
||||||
temp := cards[:0]
|
|
||||||
for _, card := range cards {
|
|
||||||
if (card.SerraCount + card.SerraCountFoil) >= count {
|
|
||||||
temp = append(temp, card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cards = temp
|
|
||||||
|
|
||||||
return cards
|
|
||||||
}
|
|
||||||
|
|
||||||
func showCardList(cards []Card, detail bool) {
|
|
||||||
|
|
||||||
var total float64
|
|
||||||
if drawImg {
|
|
||||||
for _, card := range cards {
|
|
||||||
drawImage(&card)
|
|
||||||
}
|
|
||||||
} else if detail {
|
|
||||||
for _, card := range cards {
|
|
||||||
fmt.Printf("* %dx %s%s%s (%s/%s) %s%.2f%s %s %s %s\n", card.SerraCount+card.SerraCountFoil+card.SerraCountEtched, Purple, card.Name, Reset, card.Set, card.CollectorNumber, Yellow, card.getValue(false), getCurrency(), Background, strings.Replace(card.ScryfallURI, "?utm_source=api", "", 1), Reset)
|
|
||||||
total = total + card.getValue(false)*float64(card.SerraCount) + card.getValue(true)*float64(card.SerraCountFoil)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for _, card := range cards {
|
|
||||||
fmt.Printf("* %dx (%dx) %s%s%s (%s/%s) %s%.2f%s%s\n",
|
|
||||||
card.SerraCount+card.SerraCountFoil+card.SerraCountEtched-card.SerraCountDeck-card.SerraCountFoilDeck-card.SerraCountEtchedDeck,
|
|
||||||
card.SerraCount+card.SerraCountFoil+card.SerraCountEtched,
|
|
||||||
Purple, card.Name, Reset, card.Set, card.CollectorNumber, Yellow, card.getValue(false), getCurrency(), Reset)
|
|
||||||
total = total + card.getValue(false)*float64(card.SerraCount) + card.getValue(true)*float64(card.SerraCountFoil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !drawImg {
|
|
||||||
fmt.Printf("\nTotal Value: %s%.2f%s%s\n", Yellow, total, getCurrency(), Reset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func showCardDetails(card *Card) error {
|
|
||||||
if drawImg {
|
|
||||||
drawImage(card)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%s%s%s (%s/%s)\n", Purple, card.Name, Reset, card.Set, card.CollectorNumber)
|
|
||||||
fmt.Printf("Available: %d / %d\n", card.SerraCount + card.SerraCountFoil - card.SerraCountDeck - card.SerraCountFoilDeck, card.SerraCount + card.SerraCountFoil)
|
|
||||||
fmt.Printf("Added: %s\n", stringToTime(card.SerraCreated))
|
|
||||||
fmt.Printf("Rarity: %s\n", card.Rarity)
|
|
||||||
fmt.Printf("Scryfall: %s\n", strings.Replace(card.ScryfallURI, "?utm_source=api", "", 1))
|
|
||||||
|
|
||||||
fmt.Printf("\n%sCurrent Value%s\n", Green, Reset)
|
|
||||||
fmt.Printf("* Normal: %dx %s%.2f%s%s\n", card.SerraCount, Yellow, card.getValue(false), getCurrency(), Reset)
|
|
||||||
if card.SerraCountFoil > 0 {
|
|
||||||
fmt.Printf("* Foil: %dx %s%.2f%s%s\n", card.SerraCountFoil, Yellow, card.getValue(true), getCurrency(), Reset)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\n%sValue History%s\n", Green, Reset)
|
|
||||||
showPriceHistory(card.SerraPrices, "* ", false)
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func drawImage(card *Card) {
|
|
||||||
fmt.Printf("%s - %s (%s/%s)\n", card.Name, card.SetName, card.Set, card.CollectorNumber)
|
|
||||||
data, err := base64.StdEncoding.DecodeString(card.SerraImage64)
|
|
||||||
if err == nil {
|
|
||||||
reader := bytes.NewReader(data)
|
|
||||||
img, err := png.Decode(reader)
|
|
||||||
img = resize.Resize(305, 425, img, resize.Bicubic)
|
|
||||||
if err == nil {
|
|
||||||
kittyimg.Fprintln(os.Stdout, img)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
package serra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
checkCmd.Flags().StringVarP(&set, "set", "s", "", "Filter by set code (usg/mmq/vow)")
|
|
||||||
checkCmd.Flags().BoolVarP(&detail, "detail", "d", false, "Show details for cards (url)")
|
|
||||||
rootCmd.AddCommand(checkCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
var checkCmd = &cobra.Command{
|
|
||||||
Aliases: []string{"c"},
|
|
||||||
Use: "check",
|
|
||||||
Short: "Check if a card is in your collection",
|
|
||||||
Long: "Check if a card is in your collection. Useful for list comparsions",
|
|
||||||
SilenceErrors: true,
|
|
||||||
RunE: func(cmd *cobra.Command, cards []string) error {
|
|
||||||
checkCards(cards, detail)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkCards(cards []string, detail bool) error {
|
|
||||||
client := storageConnect()
|
|
||||||
coll := &Collection{client.Database("serra").Collection("cards")}
|
|
||||||
defer storageDisconnect(client)
|
|
||||||
l := Logger()
|
|
||||||
|
|
||||||
// Loop over different cards
|
|
||||||
for _, card := range cards {
|
|
||||||
|
|
||||||
if !strings.Contains(card, "/") {
|
|
||||||
l.Errorf("Invalid card format %s. Needs to be set/collector number i.e. \"usg/13\"", card)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract collector number and set name from card input & trim any leading 0 from collector number
|
|
||||||
collectorNumber := strings.TrimLeft(strings.Split(card, "/")[1], "0")
|
|
||||||
setName := strings.ToLower(strings.Split(card, "/")[0])
|
|
||||||
|
|
||||||
if collectorNumber == "" {
|
|
||||||
l.Errorf("Invalid card format %s. Needs to be set/collector number i.e. \"usg/13\"", card)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if card is already in collection
|
|
||||||
co, err := coll.storageFind(bson.D{{"set", setName}, {"collectornumber", collectorNumber}}, bson.D{}, 0, 0)
|
|
||||||
if err != nil {
|
|
||||||
l.Warn(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If Card is in collection, print yes.
|
|
||||||
if len(co) >= 1 {
|
|
||||||
c := co[0]
|
|
||||||
fmt.Printf("PRESENT %s \"%s\" (%s, %.2f%s) %s\n", card, c.Name, c.Rarity, c.getValue(foil), getCurrency(), strings.Replace(c.ScryfallURI, "?utm_source=api", "", 1))
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
if detail {
|
|
||||||
// fetch card from scyrfall if --detail was given
|
|
||||||
c, _ := fetchCard(setName, collectorNumber)
|
|
||||||
fmt.Printf("MISSING %s \"%s\" (%s, %.2f%s) %s\n", card, c.Name, c.Rarity, c.getValue(foil), getCurrency(), strings.Replace(c.ScryfallURI, "?utm_source=api", "", 1))
|
|
||||||
} else {
|
|
||||||
// Just print, the card name was not found
|
|
||||||
fmt.Printf("MISSING \"%s\"\n", card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
storageDisconnect(client)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
package serra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
deckCmd.Flags().Int64VarP(&count, "count", "c", 1, "Amount of cards to add")
|
|
||||||
deckCmd.Flags().BoolVarP(&foil, "foil", "f", false, "Add foil variant of card")
|
|
||||||
rootCmd.AddCommand(deckCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
var deckCmd = &cobra.Command{
|
|
||||||
Aliases: []string{"d"},
|
|
||||||
Use: "deck",
|
|
||||||
Short: "Mark a card as in a deck",
|
|
||||||
Long: "Mark a card as in a deck",
|
|
||||||
SilenceErrors: true,
|
|
||||||
RunE: func(cmd *cobra.Command, cards []string) error {
|
|
||||||
deckCards(cards, count)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func deckCards(cards []string, count int64) error {
|
|
||||||
client := storageConnect()
|
|
||||||
coll := &Collection{client.Database("serra").Collection("cards")}
|
|
||||||
l := Logger()
|
|
||||||
defer storageDisconnect(client)
|
|
||||||
|
|
||||||
// Loop over different cards
|
|
||||||
for _, card := range cards {
|
|
||||||
// Extract collector number and set name from card input & trim any leading 0 from collector number
|
|
||||||
|
|
||||||
if !strings.Contains(card, "/") {
|
|
||||||
l.Errorf("Invalid card format %s. Needs to be set/collector number i.e. \"usg/13\"", card)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
setName := strings.ToLower(strings.Split(card, "/")[0])
|
|
||||||
collectorNumber := strings.TrimLeft(strings.Split(card, "/")[1], "0")
|
|
||||||
|
|
||||||
if collectorNumber == "" {
|
|
||||||
l.Errorf("Invalid card format %s. Needs to be set/collector number i.e. \"usg/13\"", card)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if card is already in collection
|
|
||||||
co, err := coll.storageFind(bson.D{{"set", setName}, {"collectornumber", collectorNumber}}, bson.D{}, 0, 0)
|
|
||||||
if err != nil {
|
|
||||||
l.Error(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(co) >= 1 {
|
|
||||||
modifyCardDeckCount(coll, &co[0], count, foil)
|
|
||||||
} else {
|
|
||||||
l.Errorf("Card not in collection: %s", card)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
storageDisconnect(client)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
package serra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
const EUR = "€"
|
|
||||||
const USD = "$"
|
|
||||||
|
|
||||||
func getMongoDBURI() string {
|
|
||||||
l := Logger()
|
|
||||||
uri := os.Getenv("MONGODB_URI")
|
|
||||||
if uri == "" {
|
|
||||||
l.Fatal("You must set 'MONGODB_URI' environmental variable. See\n\t https://docs.mongodb.com/drivers/go/current/usage-examples/#environment-variable")
|
|
||||||
}
|
|
||||||
|
|
||||||
return uri
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns configured human readable name for
|
|
||||||
// the configured currency of the user
|
|
||||||
func getCurrency() string {
|
|
||||||
l := Logger()
|
|
||||||
switch os.Getenv("SERRA_CURRENCY") {
|
|
||||||
case "EUR":
|
|
||||||
return EUR
|
|
||||||
case "USD":
|
|
||||||
return USD
|
|
||||||
default:
|
|
||||||
l.Warn("You did not configure SERRA_CURRENCY. Assuming \"USD\"")
|
|
||||||
return "$"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,127 +0,0 @@
|
|||||||
package serra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/csv"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
exportCmd.Flags().StringVarP(&set, "set", "e", "", "Filter by set code (usg/mmq/vow)")
|
|
||||||
exportCmd.Flags().StringVarP(&format, "format", "f", "tcgpowertools", "Choose format to export (tcgpowertools/json)")
|
|
||||||
exportCmd.Flags().Int64VarP(&count, "min-count", "c", 0, "Occource more than X in your collection")
|
|
||||||
rootCmd.AddCommand(exportCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
var exportCmd = &cobra.Command{
|
|
||||||
Use: "export",
|
|
||||||
Short: "Export cards from your collection",
|
|
||||||
Long: `Export cards from your collection.
|
|
||||||
Your data. Your choice.
|
|
||||||
Supports multiple output formats depending on where you want to export your collection.`,
|
|
||||||
SilenceErrors: true,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
cardList := Cards(rarity, set, sortby, name, oracle, cardType, reserved, foil, 0, 0, false)
|
|
||||||
|
|
||||||
// filter out cards that do not reach the minimum amount (--min-count)
|
|
||||||
// this is done after query result because find query constructed does not support
|
|
||||||
// aggregating fields (of count and countFoil).
|
|
||||||
temp := cardList[:0]
|
|
||||||
for _, card := range cardList {
|
|
||||||
if (card.SerraCount + card.SerraCountFoil) >= count {
|
|
||||||
temp = append(temp, card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cardList = temp
|
|
||||||
|
|
||||||
switch format {
|
|
||||||
case "tcgpowertools":
|
|
||||||
exportTCGPowertools(cardList)
|
|
||||||
case "tcghome":
|
|
||||||
exportTCGHome(cardList)
|
|
||||||
case "moxfield":
|
|
||||||
exportMoxfield(cardList)
|
|
||||||
case "json":
|
|
||||||
exportJSON(cardList)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func exportTCGPowertools(cards []Card) {
|
|
||||||
|
|
||||||
// TCGPowertools.com Example
|
|
||||||
// idProduct,quantity,name,set,condition,language,isFoil,isPlayset,isSigned,isFirstEd,price,comment
|
|
||||||
// 260009,1,Totally Lost,Gatecrash,GD,English,true,true,,,1000,
|
|
||||||
// 260009,1,Totally Lost,Gatecrash,NM,English,true,true,,,1000,
|
|
||||||
|
|
||||||
fmt.Println("quantity,cardmarketId,name,set,condition,language,isFoil,isPlayset,price,comment")
|
|
||||||
for _, card := range cards {
|
|
||||||
fmt.Printf("%d,%.0f,%s,%s,EX,German,false,false,%.2f,\n", card.SerraCount+card.SerraCountFoil, card.CardmarketID, card.Name, card.SetName, card.getValue(false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func exportMoxfield(cards []Card) {
|
|
||||||
|
|
||||||
// Structure
|
|
||||||
// https://www.moxfield.com/help/importing-collection
|
|
||||||
|
|
||||||
records := [][]string{{
|
|
||||||
"Count", "Name", "Edition", "Condition", "Language", "Foil", "Collector Number", "Alter", "Proxy", "Purchase Price"}}
|
|
||||||
|
|
||||||
w := csv.NewWriter(os.Stdout)
|
|
||||||
|
|
||||||
for _, card := range cards {
|
|
||||||
records = append(records,
|
|
||||||
[]string{fmt.Sprintf("%d", card.SerraCount+card.SerraCountFoil), card.Name, card.Set, "NM", "English", "FALSE", card.CollectorNumber, "FALSE", "FALSE", ""})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, record := range records {
|
|
||||||
if err := w.Write(record); err != nil {
|
|
||||||
log.Fatalln("error writing record to csv:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Flush()
|
|
||||||
|
|
||||||
if err := w.Error(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func exportTCGHome(cards []Card) {
|
|
||||||
|
|
||||||
// Strucutre
|
|
||||||
// https://app.tcg-home.com/e686ea62-7078-4f52-bd6f-515e18c7dc6a
|
|
||||||
|
|
||||||
records := [][]string{{
|
|
||||||
"amount", "name", "finish", "set", "collector_number", "language", "condition", "scryfall_id", "purchase_price"}}
|
|
||||||
|
|
||||||
w := csv.NewWriter(os.Stdout)
|
|
||||||
|
|
||||||
for _, card := range cards {
|
|
||||||
records = append(records,
|
|
||||||
[]string{fmt.Sprintf("%d", card.SerraCount+card.SerraCountFoil), card.Name, "", card.Set, card.CollectorNumber, "English", "EX", card.ID, ""})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, record := range records {
|
|
||||||
if err := w.Write(record); err != nil {
|
|
||||||
log.Fatalln("error writing record to csv:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Flush()
|
|
||||||
|
|
||||||
if err := w.Error(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func exportJSON(cards []Card) {
|
|
||||||
ehj, _ := json.MarshalIndent(cards, "", " ")
|
|
||||||
fmt.Println(string(ehj))
|
|
||||||
}
|
|
||||||
@ -1,323 +0,0 @@
|
|||||||
package serra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/log"
|
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Rarities struct {
|
|
||||||
Rares, Uncommons, Commons, Mythics float64
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
Icon = "\U0001F9D9\U0001F3FC"
|
|
||||||
Reset = "\033[0m"
|
|
||||||
Background = "\033[38;5;59m"
|
|
||||||
CurrentLine = "\033[38;5;60m"
|
|
||||||
Foreground = "\033[38;5;231m"
|
|
||||||
Comment = "\033[38;5;103m"
|
|
||||||
Cyan = "\033[38;5;159m"
|
|
||||||
Green = "\033[38;5;120m"
|
|
||||||
Orange = "\033[38;5;222m"
|
|
||||||
Pink = "\033[38;5;212m"
|
|
||||||
Purple = "\033[38;5;183m"
|
|
||||||
Red = "\033[38;5;210m"
|
|
||||||
Yellow = "\033[38;5;229m"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Logger() *log.Logger {
|
|
||||||
|
|
||||||
l := log.New(os.Stderr)
|
|
||||||
l.SetReportTimestamp(false)
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
func getStoredCard(coll *Collection, c *Card) (Card, error) {
|
|
||||||
// find already existing card
|
|
||||||
sort := bson.D{{"_id", 1}}
|
|
||||||
searchFilter := bson.D{{"_id", c.ID}}
|
|
||||||
storedCards, err := coll.storageFind(searchFilter, sort, 0, 0)
|
|
||||||
if err != nil {
|
|
||||||
return Card{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return storedCards[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func modifyCardCount(coll *Collection, c *Card, amount int64, foil bool) error {
|
|
||||||
l := Logger()
|
|
||||||
|
|
||||||
storedCard, err := getStoredCard(coll, c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// update card amount
|
|
||||||
var update bson.M
|
|
||||||
if foil {
|
|
||||||
update = bson.M{
|
|
||||||
"$set": bson.M{"serra_count_foil": storedCard.SerraCountFoil + amount},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
update = bson.M{
|
|
||||||
"$set": bson.M{"serra_count": storedCard.SerraCount + amount},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coll.storageUpdate(bson.M{"_id": bson.M{"$eq": c.ID}}, update)
|
|
||||||
|
|
||||||
var total int64
|
|
||||||
if foil {
|
|
||||||
total = storedCard.SerraCountFoil + amount
|
|
||||||
if amount < 0 {
|
|
||||||
l.Warnf("Reduced card amount of \"%s\" (%.2f%s, foil) from %d to %d", storedCard.Name, storedCard.getValue(true), getCurrency(), storedCard.SerraCountFoil, total)
|
|
||||||
} else {
|
|
||||||
l.Warnf("Increased card amount of \"%s\" (%.2f%s, foil) from %d to %d", storedCard.Name, storedCard.getValue(true), getCurrency(), storedCard.SerraCountFoil, total)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
total = storedCard.SerraCount + amount
|
|
||||||
if amount < 0 {
|
|
||||||
l.Warnf("Reduced card amount of \"%s\" (%.2f%s) from %d to %d", storedCard.Name, storedCard.getValue(false), getCurrency(), storedCard.SerraCount, total)
|
|
||||||
} else {
|
|
||||||
l.Warnf("Increased card amount of \"%s\" (%.2f%s) from %d to %d", storedCard.Name, storedCard.getValue(false), getCurrency(), storedCard.SerraCount, total)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func modifyCardDeckCount(coll *Collection, c *Card, amount int64, foil bool) error {
|
|
||||||
l := Logger()
|
|
||||||
|
|
||||||
storedCard, err := getStoredCard(coll, c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// update card amount
|
|
||||||
var update bson.M
|
|
||||||
if foil {
|
|
||||||
newAmount := min(max(storedCard.SerraCountFoilDeck + amount, 0), storedCard.SerraCountFoil)
|
|
||||||
l.Infof("%d / %d available", storedCard.SerraCountFoil - newAmount, storedCard.SerraCountFoil)
|
|
||||||
update = bson.M{
|
|
||||||
"$set": bson.M{"serra_count_foil_deck": newAmount},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newAmount := min(max(storedCard.SerraCountDeck + amount, 0), storedCard.SerraCount)
|
|
||||||
l.Infof("%d / %d available", storedCard.SerraCount - newAmount, storedCard.SerraCount)
|
|
||||||
update = bson.M{
|
|
||||||
"$set": bson.M{"serra_count_deck": newAmount},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coll.storageUpdate(bson.M{"_id": bson.M{"$eq": c.ID}}, update)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findCardByCollectorNumber(coll *Collection, setCode string, collectorNumber string) (*Card, error) {
|
|
||||||
sort := bson.D{{"_id", 1}}
|
|
||||||
searchFilter := bson.D{{"set", setCode}, {"collectornumber", collectorNumber}}
|
|
||||||
storedCards, err := coll.storageFind(searchFilter, sort, 0, 0)
|
|
||||||
if err != nil {
|
|
||||||
return &Card{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(storedCards) < 1 {
|
|
||||||
return &Card{}, errors.New("Card not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &storedCards[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringToTime(s primitive.DateTime) string {
|
|
||||||
return time.UnixMilli(int64(s)).Format("2006-01-02")
|
|
||||||
}
|
|
||||||
|
|
||||||
// missing compares two slices and returns slice of differences
|
|
||||||
func missing(a, b []string) []string {
|
|
||||||
type void struct{}
|
|
||||||
// create map with length of the 'a' slice
|
|
||||||
ma := make(map[string]void, len(a))
|
|
||||||
diffs := []string{}
|
|
||||||
// Convert first slice to map with empty struct (0 bytes)
|
|
||||||
for _, ka := range a {
|
|
||||||
ma[ka] = void{}
|
|
||||||
}
|
|
||||||
// find missing values in a
|
|
||||||
for _, kb := range b {
|
|
||||||
if _, ok := ma[kb]; !ok {
|
|
||||||
diffs = append(diffs, kb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return diffs
|
|
||||||
}
|
|
||||||
|
|
||||||
func findSetByCode(coll *Collection, setcode string) (*Set, error) {
|
|
||||||
storedSets, err := coll.storageFindSet(bson.D{{"code", setcode}}, bson.D{{"_id", 1}})
|
|
||||||
if err != nil {
|
|
||||||
return &Set{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(storedSets) < 1 {
|
|
||||||
return &Set{}, errors.New("Set not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &storedSets[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertManaSymbols(sym []interface{}) string {
|
|
||||||
var mana string
|
|
||||||
|
|
||||||
if len(sym) == 0 {
|
|
||||||
mana = mana + "None" //probibited sign for lands
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range sym {
|
|
||||||
switch v {
|
|
||||||
case "B":
|
|
||||||
mana = mana + "Black" //black
|
|
||||||
case "R":
|
|
||||||
mana = mana + "Red" //red
|
|
||||||
case "G":
|
|
||||||
mana = mana + "Green" //green
|
|
||||||
case "U":
|
|
||||||
mana = mana + "Blue" //blue
|
|
||||||
case "W":
|
|
||||||
mana = mana + "White" //white
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mana
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertRarities(rar []primitive.M) Rarities {
|
|
||||||
|
|
||||||
// this is maybe the ugliest way someone could choose to verify, if a rarity type is missing
|
|
||||||
// [
|
|
||||||
// { _id: { rarity: 'common' }, count: 20 },
|
|
||||||
// { _id: { rarity: 'uncommon' }, count: 2 }
|
|
||||||
// ]
|
|
||||||
// if a result like this is there, 1 rarity type "rare" is not in the array. and needs to be
|
|
||||||
// initialized with 0, otherwise we get a panic
|
|
||||||
|
|
||||||
var ri Rarities
|
|
||||||
for _, r := range rar {
|
|
||||||
switch r["_id"] {
|
|
||||||
case "rare":
|
|
||||||
ri.Rares = r["count"].(float64)
|
|
||||||
case "uncommon":
|
|
||||||
ri.Uncommons = r["count"].(float64)
|
|
||||||
case "common":
|
|
||||||
ri.Commons = r["count"].(float64)
|
|
||||||
case "mythic":
|
|
||||||
ri.Mythics = r["count"].(float64)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ri
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func showPriceHistory(prices []PriceEntry, prefix string, total bool) {
|
|
||||||
|
|
||||||
var before float64
|
|
||||||
for _, e := range prices {
|
|
||||||
|
|
||||||
var value float64
|
|
||||||
if total {
|
|
||||||
if getCurrency() == EUR {
|
|
||||||
value = e.Eur + e.EurFoil
|
|
||||||
} else {
|
|
||||||
value = e.Usd + e.UsdFoil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if getCurrency() == EUR {
|
|
||||||
value = e.Eur
|
|
||||||
} else {
|
|
||||||
value = e.Usd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if value > before && before != 0 {
|
|
||||||
fmt.Printf("%s%s%s %.2f%s%s (%+.2f%%, %+.2f%s)\n", prefix, stringToTime(e.Date), Green, value, getCurrency(), Reset, (value/before*100)-100, value-before, getCurrency())
|
|
||||||
} else if value < before {
|
|
||||||
fmt.Printf("%s%s%s %.2f%s%s (%+.2f%%, %+.2f%s)\n", prefix, stringToTime(e.Date), Red, value, getCurrency(), Reset, (value/before*100)-100, value-before, getCurrency())
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%s%s %.2f%s%s\n", prefix, stringToTime(e.Date), value, getCurrency(), Reset)
|
|
||||||
}
|
|
||||||
before = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterForDigits(str string) int {
|
|
||||||
var numStr string
|
|
||||||
for _, c := range str {
|
|
||||||
if unicode.IsDigit(c) {
|
|
||||||
numStr += string(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s, _ := strconv.Atoi(numStr)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFloat64(unknown interface{}) (float64, error) {
|
|
||||||
switch i := unknown.(type) {
|
|
||||||
case float64:
|
|
||||||
return i, nil
|
|
||||||
case float32:
|
|
||||||
return float64(i), nil
|
|
||||||
case int64:
|
|
||||||
return float64(i), nil
|
|
||||||
case int32:
|
|
||||||
return float64(i), nil
|
|
||||||
case int:
|
|
||||||
return float64(i), nil
|
|
||||||
case uint64:
|
|
||||||
return float64(i), nil
|
|
||||||
case uint32:
|
|
||||||
return float64(i), nil
|
|
||||||
case uint:
|
|
||||||
return float64(i), nil
|
|
||||||
default:
|
|
||||||
return math.NaN(), errors.New("non-numeric type could not be converted to float")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func coloredValue(value float64) string {
|
|
||||||
|
|
||||||
outputColor := Reset
|
|
||||||
|
|
||||||
if value > 1 {
|
|
||||||
outputColor = Green
|
|
||||||
}
|
|
||||||
if value > 5 {
|
|
||||||
outputColor = Yellow
|
|
||||||
}
|
|
||||||
if value > 10 {
|
|
||||||
outputColor = Red
|
|
||||||
}
|
|
||||||
|
|
||||||
return outputColor
|
|
||||||
}
|
|
||||||
|
|
||||||
func askConfirmation(card *Card) bool {
|
|
||||||
drawImage(card)
|
|
||||||
|
|
||||||
fmt.Println("Is this correct (y/n)?")
|
|
||||||
|
|
||||||
var char = 'x'
|
|
||||||
for char != 'y' && char != 'n' {
|
|
||||||
fmt.Scanf("%c\n", &char)
|
|
||||||
}
|
|
||||||
|
|
||||||
return char == 'y'
|
|
||||||
}
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
package serra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(missingCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
var missingCmd = &cobra.Command{
|
|
||||||
Aliases: []string{"m"},
|
|
||||||
Use: "missing <set>",
|
|
||||||
Short: "Display missing cards from a set",
|
|
||||||
Long: `In case you are a set collector, you can generate a list of
|
|
||||||
cards you dont own (yet) :)`,
|
|
||||||
SilenceErrors: true,
|
|
||||||
RunE: func(cmd *cobra.Command, setName []string) error {
|
|
||||||
client := storageConnect()
|
|
||||||
coll := &Collection{client.Database("serra").Collection("cards")}
|
|
||||||
l := Logger()
|
|
||||||
defer storageDisconnect(client)
|
|
||||||
|
|
||||||
// fetch all cards in set
|
|
||||||
cards, err := coll.storageFind(bson.D{{"set", setName[0]}}, bson.D{{"collectornumber", 1}}, 0, 0)
|
|
||||||
if (err != nil) || len(cards) == 0 {
|
|
||||||
l.Errorf("Set %s not found or no card in your collection.", setName[0])
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch set informations
|
|
||||||
setcoll := &Collection{client.Database("serra").Collection("sets")}
|
|
||||||
sets, _ := setcoll.storageFindSet(bson.D{{"code", setName[0]}}, bson.D{{"_id", 1}})
|
|
||||||
set := sets[0]
|
|
||||||
|
|
||||||
fmt.Printf("Missing cards in %s\n", sets[0].Name)
|
|
||||||
|
|
||||||
// generate set with all setnumbers
|
|
||||||
var (
|
|
||||||
completeSet []string
|
|
||||||
i int64
|
|
||||||
)
|
|
||||||
for i = 1; i <= set.CardCount; i++ {
|
|
||||||
completeSet = append(completeSet, strconv.FormatInt(i, 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
// iterate over all cards in collection
|
|
||||||
var inCollection []string
|
|
||||||
for _, c := range cards {
|
|
||||||
inCollection = append(inCollection, c.CollectorNumber)
|
|
||||||
}
|
|
||||||
|
|
||||||
misses := missing(inCollection, completeSet)
|
|
||||||
|
|
||||||
// Fetch all missing cards
|
|
||||||
missingCards := []*Card{}
|
|
||||||
for _, m := range misses {
|
|
||||||
card, err := fetchCard(setName[0], m)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
missingCards = append(missingCards, card)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort the missing cards by ID
|
|
||||||
sort.Slice(missingCards, func(i, j int) bool {
|
|
||||||
id1, _ := strconv.Atoi(missingCards[i].CollectorNumber)
|
|
||||||
id2, _ := strconv.Atoi(missingCards[j].CollectorNumber)
|
|
||||||
return id1 < id2
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, card := range missingCards {
|
|
||||||
fmt.Printf("%s%s/%s\t%s(%s, %shttps://scryfall.com/card/%s/%s%s)\t%s%.02f%s%s\t%s (%s)\n", Purple, card.Set, card.CollectorNumber, Reset, string([]rune(card.Rarity)[0]), Background, card.Set, card.CollectorNumber, Reset, Green, card.getValue(false), Reset, getCurrency(), card.Name, card.SetName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
// Package serra
|
|
||||||
//
|
|
||||||
// It implements base functions and also cli wrappers
|
|
||||||
// The entire tool consists only of this one package.
|
|
||||||
package serra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
Version = "unknown"
|
|
||||||
address string
|
|
||||||
artist string
|
|
||||||
cardType string
|
|
||||||
color string
|
|
||||||
cmc int64
|
|
||||||
count int64
|
|
||||||
detail bool
|
|
||||||
drawImg bool
|
|
||||||
foil bool
|
|
||||||
format string
|
|
||||||
interactive bool
|
|
||||||
limit float64
|
|
||||||
name string
|
|
||||||
omitInDeck bool
|
|
||||||
oracle string
|
|
||||||
port uint64
|
|
||||||
rarity string
|
|
||||||
reserved bool
|
|
||||||
set string
|
|
||||||
sinceBeginning bool
|
|
||||||
sinceLastUpdate bool
|
|
||||||
sortby string
|
|
||||||
unique bool
|
|
||||||
)
|
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
|
||||||
Version: Version,
|
|
||||||
Long: `serra - Magic: The Gathering Collection Tracker`,
|
|
||||||
Use: "serra",
|
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
SilenceErrors: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func Execute() {
|
|
||||||
|
|
||||||
l := Logger()
|
|
||||||
if err := rootCmd.Execute(); err != nil {
|
|
||||||
l.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,245 +0,0 @@
|
|||||||
package serra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/mitchellh/mapstructure"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(statsCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
var statsCmd = &cobra.Command{
|
|
||||||
Aliases: []string{"stats"},
|
|
||||||
Use: "stats",
|
|
||||||
Short: "Shows statistics of the collection",
|
|
||||||
SilenceErrors: true,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
Stats()
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func Stats() {
|
|
||||||
client := storageConnect()
|
|
||||||
coll := &Collection{client.Database("serra").Collection("cards")}
|
|
||||||
totalcoll := &Collection{client.Database("serra").Collection("total")}
|
|
||||||
defer storageDisconnect(client)
|
|
||||||
|
|
||||||
// Show Value Stats
|
|
||||||
showValueStats(coll, totalcoll)
|
|
||||||
|
|
||||||
// Reserved List
|
|
||||||
showReservedListStats(coll)
|
|
||||||
|
|
||||||
// Rarities
|
|
||||||
showRarityStats(coll)
|
|
||||||
|
|
||||||
// Colors
|
|
||||||
showColorStats(coll)
|
|
||||||
|
|
||||||
// Artists
|
|
||||||
showArtistStats(coll)
|
|
||||||
|
|
||||||
// Mana Curve of Collection
|
|
||||||
showManaCurveStats(coll)
|
|
||||||
|
|
||||||
// Show cards added per month
|
|
||||||
showCardsAddedPerMonth(coll)
|
|
||||||
}
|
|
||||||
|
|
||||||
func showValueStats(coll *Collection, totalcoll *Collection) {
|
|
||||||
l := Logger()
|
|
||||||
// Value and Card Numbers
|
|
||||||
stats, _ := coll.storageAggregate(mongo.Pipeline{
|
|
||||||
bson.D{
|
|
||||||
{"$group", bson.D{
|
|
||||||
{"_id", nil},
|
|
||||||
{"value", bson.D{{"$sum", bson.D{{"$multiply", bson.A{getCurrencyField(false), "$serra_count"}}}}}},
|
|
||||||
{"value_foil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{getCurrencyField(true), "$serra_count_foil"}}}}}},
|
|
||||||
{"count", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count"}}}}}},
|
|
||||||
{"count_foil", bson.D{{"$sum", "$serra_count_foil"}}},
|
|
||||||
{"rarity", bson.D{{"$sum", "$rarity"}}},
|
|
||||||
{"unique", bson.D{{"$sum", 1}}},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
bson.D{
|
|
||||||
{"$addFields", bson.D{
|
|
||||||
{"count_all", bson.D{{"$sum", bson.A{"$count", "$count_foil"}}}},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
fmt.Printf("%sCards %s\n", Green, Reset)
|
|
||||||
fmt.Printf("Total: %s%.0f%s\n", Yellow, stats[0]["count_all"], Reset)
|
|
||||||
fmt.Printf("Unique: %s%d%s\n", Purple, stats[0]["unique"], Reset)
|
|
||||||
fmt.Printf("Normal: %s%.0f%s\n", Purple, stats[0]["count"], Reset)
|
|
||||||
fmt.Printf("Foil: %s%d%s\n", Purple, stats[0]["count_foil"], Reset)
|
|
||||||
|
|
||||||
// Total Value
|
|
||||||
fmt.Printf("\n%sTotal Value%s\n", Green, Reset)
|
|
||||||
normalValue, err := getFloat64(stats[0]["value"])
|
|
||||||
if err != nil {
|
|
||||||
l.Error(err)
|
|
||||||
normalValue = 0
|
|
||||||
}
|
|
||||||
foilValue, err := getFloat64(stats[0]["value_foil"])
|
|
||||||
if err != nil {
|
|
||||||
l.Error(err)
|
|
||||||
foilValue = 0
|
|
||||||
}
|
|
||||||
countAll, err := getFloat64(stats[0]["count_all"])
|
|
||||||
if err != nil {
|
|
||||||
l.Error(err)
|
|
||||||
foilValue = 0
|
|
||||||
}
|
|
||||||
totalValue := normalValue + foilValue
|
|
||||||
fmt.Printf("Total: %s%.2f%s%s\n", Pink, totalValue, getCurrency(), Reset)
|
|
||||||
fmt.Printf("Normal: %s%.2f%s%s\n", Pink, normalValue, getCurrency(), Reset)
|
|
||||||
fmt.Printf("Foils: %s%.2f%s%s\n", Pink, foilValue, getCurrency(), Reset)
|
|
||||||
fmt.Printf("Average Card: %s%.2f%s%s\n", Pink, totalValue/countAll, getCurrency(), Reset)
|
|
||||||
total, _ := totalcoll.storageFindTotal()
|
|
||||||
|
|
||||||
fmt.Printf("History: \n")
|
|
||||||
showPriceHistory(total.Value, "* ", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func showReservedListStats(coll *Collection) {
|
|
||||||
reserved, _ := coll.storageAggregate(mongo.Pipeline{
|
|
||||||
bson.D{
|
|
||||||
{"$match", bson.D{
|
|
||||||
{"reserved", true}}}},
|
|
||||||
bson.D{
|
|
||||||
{"$group", bson.D{
|
|
||||||
{"_id", nil},
|
|
||||||
{"count", bson.D{{"$sum", 1}}},
|
|
||||||
}}},
|
|
||||||
})
|
|
||||||
|
|
||||||
var countReserved int32
|
|
||||||
if len(reserved) > 0 {
|
|
||||||
countReserved = reserved[0]["count"].(int32)
|
|
||||||
}
|
|
||||||
fmt.Printf("Reserved List: %s%d%s\n", Yellow, countReserved, Reset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func showRarityStats(coll *Collection) {
|
|
||||||
rar, _ := coll.storageAggregate(mongo.Pipeline{
|
|
||||||
bson.D{
|
|
||||||
{"$group", bson.D{
|
|
||||||
{"_id", "$rarity"},
|
|
||||||
{"count", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count"}}}}}},
|
|
||||||
}}},
|
|
||||||
bson.D{
|
|
||||||
{"$sort", bson.D{
|
|
||||||
{"_id", 1},
|
|
||||||
}}},
|
|
||||||
})
|
|
||||||
ri := convertRarities(rar)
|
|
||||||
fmt.Printf("\n%sRarity%s\n", Green, Reset)
|
|
||||||
fmt.Printf("Mythics: %s%.0f%s\n", Pink, ri.Mythics, Reset)
|
|
||||||
fmt.Printf("Rares: %s%.0f%s\n", Pink, ri.Rares, Reset)
|
|
||||||
fmt.Printf("Uncommons: %s%.0f%s\n", Yellow, ri.Uncommons, Reset)
|
|
||||||
fmt.Printf("Commons: %s%.0f%s\n", Purple, ri.Commons, Reset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func showCardsAddedPerMonth(coll *Collection) {
|
|
||||||
fmt.Printf("\n%sCards added over time%s\n", Green, Reset)
|
|
||||||
type Caot struct {
|
|
||||||
ID struct {
|
|
||||||
Year int32 `mapstructure:"year"`
|
|
||||||
Month int32 `mapstructure:"month"`
|
|
||||||
} `mapstructure:"_id"`
|
|
||||||
Count int32 `mapstructure:"count"`
|
|
||||||
}
|
|
||||||
caot, _ := coll.storageAggregate(mongo.Pipeline{
|
|
||||||
bson.D{
|
|
||||||
{"$project", bson.D{
|
|
||||||
{"month", bson.D{
|
|
||||||
{"$month", "$serra_created"}}},
|
|
||||||
{"year", bson.D{
|
|
||||||
{"$year", "$serra_created"}},
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
bson.D{
|
|
||||||
{"$group", bson.D{
|
|
||||||
{"_id", bson.D{{"month", "$month"}, {"year", "$year"}}},
|
|
||||||
{"count", bson.D{{"$sum", 1}}},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
bson.D{
|
|
||||||
{"$sort", bson.D{{"_id.year", 1}, {"_id.month", 1}}},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
for _, mo := range caot {
|
|
||||||
moo := new(Caot)
|
|
||||||
mapstructure.Decode(mo, moo)
|
|
||||||
fmt.Printf("%d-%02d: %s%d%s\n", moo.ID.Year, moo.ID.Month, Purple, moo.Count, Reset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func showManaCurveStats(coll *Collection) {
|
|
||||||
cmc, _ := coll.storageAggregate(mongo.Pipeline{
|
|
||||||
bson.D{
|
|
||||||
{"$group", bson.D{
|
|
||||||
{"_id", "$cmc"},
|
|
||||||
{"count", bson.D{{"$sum", 1}}},
|
|
||||||
}}},
|
|
||||||
bson.D{
|
|
||||||
{"$sort", bson.D{
|
|
||||||
{"_id", 1},
|
|
||||||
}}},
|
|
||||||
})
|
|
||||||
fmt.Printf("\n%sMana Curve%s\n", Green, Reset)
|
|
||||||
for _, mc := range cmc {
|
|
||||||
fmt.Printf("%.0f: %s%d%s\n", mc["_id"], Purple, mc["count"], Reset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func showArtistStats(coll *Collection) {
|
|
||||||
artists, _ := coll.storageAggregate(mongo.Pipeline{
|
|
||||||
bson.D{
|
|
||||||
{"$group", bson.D{
|
|
||||||
{"_id", "$artist"},
|
|
||||||
{"count", bson.D{{"$sum", 1}}},
|
|
||||||
}}},
|
|
||||||
bson.D{
|
|
||||||
{"$sort", bson.D{
|
|
||||||
{"count", -1},
|
|
||||||
}}},
|
|
||||||
bson.D{
|
|
||||||
{"$limit", 10}},
|
|
||||||
})
|
|
||||||
fmt.Printf("\n%sTop Artists%s\n", Green, Reset)
|
|
||||||
for _, artist := range artists {
|
|
||||||
fmt.Printf("%s: %s%d%s\n", artist["_id"].(string), Purple, artist["count"], Reset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func showColorStats(coll *Collection) {
|
|
||||||
sets, _ := coll.storageAggregate(mongo.Pipeline{
|
|
||||||
bson.D{
|
|
||||||
{"$match", bson.D{
|
|
||||||
{"coloridentity", bson.D{{"$size", 1}}}}}},
|
|
||||||
bson.D{
|
|
||||||
{"$group", bson.D{
|
|
||||||
{"_id", "$coloridentity"},
|
|
||||||
{"count", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count"}}}}}},
|
|
||||||
}}},
|
|
||||||
bson.D{
|
|
||||||
{"$sort", bson.D{
|
|
||||||
{"count", -1},
|
|
||||||
}}},
|
|
||||||
})
|
|
||||||
|
|
||||||
fmt.Printf("\n%sColors%s\n", Green, Reset)
|
|
||||||
for _, set := range sets {
|
|
||||||
x, _ := set["_id"].(primitive.A)
|
|
||||||
s := []interface{}(x)
|
|
||||||
fmt.Printf("%s: %s%.0f%s\n", convertManaSymbols(s), Purple, set["count"], Reset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
package serra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
undeckCmd.Flags().Int64VarP(&count, "count", "c", 1, "Amount of cards to add")
|
|
||||||
undeckCmd.Flags().BoolVarP(&foil, "foil", "f", false, "Add foil variant of card")
|
|
||||||
rootCmd.AddCommand(undeckCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
var undeckCmd = &cobra.Command{
|
|
||||||
Aliases: []string{"u"},
|
|
||||||
Use: "undeck",
|
|
||||||
Short: "Unmark a card as in a deck",
|
|
||||||
Long: "Unmark a card as in a deck",
|
|
||||||
SilenceErrors: true,
|
|
||||||
RunE: func(cmd *cobra.Command, cards []string) error {
|
|
||||||
deckCards(cards, -count)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
121
pkg/serra/web.go
121
pkg/serra/web.go
@ -1,121 +0,0 @@
|
|||||||
package serra
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
webCmd.Flags().StringVarP(&address, "address", "a", "0.0.0.0", "Address to listen on")
|
|
||||||
webCmd.Flags().Uint64VarP(&port, "port", "p", 8080, "Port to listen on")
|
|
||||||
rootCmd.AddCommand(webCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func add(a, b int64) int64 {
|
|
||||||
return a + b
|
|
||||||
}
|
|
||||||
|
|
||||||
var webCmd = &cobra.Command{
|
|
||||||
Aliases: []string{"a"},
|
|
||||||
Use: "web",
|
|
||||||
Short: "Startup web interface",
|
|
||||||
Long: "Start a tiny web interface to have a web view of your collection",
|
|
||||||
SilenceErrors: true,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
startWeb()
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query struct {
|
|
||||||
Name string `form:"name"`
|
|
||||||
Set string `form:"set"`
|
|
||||||
Sort string `form:"sort"`
|
|
||||||
Limit int64 `form:"limit"`
|
|
||||||
Page int64 `form:"page"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func startWeb() error {
|
|
||||||
router := gin.Default()
|
|
||||||
router.SetFuncMap(template.FuncMap{
|
|
||||||
"add": add,
|
|
||||||
})
|
|
||||||
router.LoadHTMLGlob("templates/*.tmpl")
|
|
||||||
router.Static("/assets", "./assets")
|
|
||||||
|
|
||||||
// Landing page
|
|
||||||
router.GET("/", landingPage)
|
|
||||||
|
|
||||||
router.Run(address + ":" + strconv.FormatUint(port, 10))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func landingPage(c *gin.Context) {
|
|
||||||
var query Query
|
|
||||||
if c.ShouldBind(&query) == nil {
|
|
||||||
|
|
||||||
// Construct per Page results "limit"
|
|
||||||
strLimit := c.DefaultQuery("limit", "500")
|
|
||||||
limit, _ := strconv.ParseInt(strLimit, 10, 64)
|
|
||||||
if limit == 0 {
|
|
||||||
limit = 500
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch all sets for Dropdown
|
|
||||||
sets := Sets("release")
|
|
||||||
|
|
||||||
// Fetch all results based on filter criteria
|
|
||||||
cards := Cards("", query.Set, query.Sort, query.Name, "", "", false, false, query.Page*int64(limit), limit, false)
|
|
||||||
|
|
||||||
// Construct quick way for counting results
|
|
||||||
filter := bson.D{}
|
|
||||||
client := storageConnect()
|
|
||||||
coll := &Collection{client.Database("serra").Collection("cards")}
|
|
||||||
|
|
||||||
if query.Set != "" {
|
|
||||||
filter = append(filter, bson.E{"set", query.Set})
|
|
||||||
}
|
|
||||||
|
|
||||||
if query.Name != "" {
|
|
||||||
filter = append(filter, bson.E{"name", bson.D{{"$regex", ".*" + query.Name + ".*"}, {"$options", "i"}}})
|
|
||||||
}
|
|
||||||
|
|
||||||
counts, _ := coll.storageAggregate(mongo.Pipeline{
|
|
||||||
bson.D{
|
|
||||||
{"$match", filter},
|
|
||||||
},
|
|
||||||
bson.D{
|
|
||||||
{"$group", bson.D{
|
|
||||||
{"_id", nil},
|
|
||||||
{"count", bson.D{{"$sum", 1}}},
|
|
||||||
}}},
|
|
||||||
})
|
|
||||||
defer storageDisconnect(client)
|
|
||||||
|
|
||||||
// Catch index error on no results
|
|
||||||
var numCards int32
|
|
||||||
if len(counts) != 0 {
|
|
||||||
numCards = counts[0]["count"].(int32)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "index.tmpl", gin.H{
|
|
||||||
"title": "Serra",
|
|
||||||
"cards": cards,
|
|
||||||
"sets": sets,
|
|
||||||
"query": query,
|
|
||||||
"version": Version,
|
|
||||||
"prevPage": query.Page - 1,
|
|
||||||
"page": query.Page,
|
|
||||||
"nextPage": query.Page + 1,
|
|
||||||
"limit": limit,
|
|
||||||
"numCards": int64(numCards),
|
|
||||||
"numPages": int64(numCards) / limit,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
73
readme.md
73
readme.md
@ -22,12 +22,11 @@ an overview in what cards you own and what value they have.
|
|||||||
|
|
||||||
**What Serra does not**
|
**What Serra does not**
|
||||||
|
|
||||||
* Does not care about conditions (NM, M, GD...)
|
* Does not give a shit about conditions (NM, M, GD...)
|
||||||
* Does not track etched cards. Only normal and foil.
|
|
||||||
|
|
||||||
# Quickstart
|
# Quickstart
|
||||||
|
|
||||||
## Install Binaries
|
### Install Binaries
|
||||||
|
|
||||||
on macOS you can use
|
on macOS you can use
|
||||||
|
|
||||||
@ -37,15 +36,16 @@ on Linux/BSD/Windows you can download binaries from
|
|||||||
|
|
||||||
https://github.com/noqqe/serra/releases
|
https://github.com/noqqe/serra/releases
|
||||||
|
|
||||||
## Spin up Database
|
### Spin up Database
|
||||||
|
|
||||||
To run serra, a MongoDB Database is required. The best way is to setup one by yourself. Any way it connects is fine.
|
To run serra, a MongoDB Database is required. The best way is to setup one by yourself. Any way it connects is fine.
|
||||||
|
|
||||||
|
|
||||||
You can also use the docker-compose setup included in this Repo:
|
You can also use the docker-compose setup included in this Repo:
|
||||||
|
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
|
||||||
## Configure the Database
|
### Configure the Database
|
||||||
|
|
||||||
Configure `serra` via Environment variables
|
Configure `serra` via Environment variables
|
||||||
|
|
||||||
@ -69,7 +69,6 @@ Usage:
|
|||||||
Available Commands:
|
Available Commands:
|
||||||
add Add a card to your collection
|
add Add a card to your collection
|
||||||
card Search & show cards from your collection
|
card Search & show cards from your collection
|
||||||
check Check if a card is in your collection
|
|
||||||
completion Generate the autocompletion script for the specified shell
|
completion Generate the autocompletion script for the specified shell
|
||||||
flops What cards lost most value
|
flops What cards lost most value
|
||||||
help Help about any command
|
help Help about any command
|
||||||
@ -138,12 +137,6 @@ update. After all Sets are updated, the whole collection value is updated.
|
|||||||
|
|
||||||

|

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

|
|
||||||
|
|
||||||
## Adding all those cards, manually?
|
## Adding all those cards, manually?
|
||||||
|
|
||||||
Yes. While there are serveral OCR/Photo Scanners for mtg cards, I found they
|
Yes. While there are serveral OCR/Photo Scanners for mtg cards, I found they
|
||||||
@ -163,65 +156,9 @@ one> 3
|
|||||||
1x "Apostle of Invasion" (uncommon, 0.03 USD) added to Collection.
|
1x "Apostle of Invasion" (uncommon, 0.03 USD) added to Collection.
|
||||||
```
|
```
|
||||||
|
|
||||||
It also supports ranges of cards
|
|
||||||
```
|
|
||||||
dmr> 1-3
|
|
||||||
1x "Auramancer" (common, 0.02$) added to Collection.
|
|
||||||
1x "Battle Screech" (uncommon, 0.09$) added to Collection.
|
|
||||||
1x "Cleric of the Forward Order" (common, 0.01$) added to Collection.
|
|
||||||
```
|
|
||||||
|
|
||||||
Its basically typing 2-3 digit numbers and hitting enter. I was way faster
|
Its basically typing 2-3 digit numbers and hitting enter. I was way faster
|
||||||
with this approach then Smartphone scanners.
|
with this approach then Smartphone scanners.
|
||||||
|
|
||||||
# Upgrade
|
|
||||||
|
|
||||||
If you want to upgrade, go to [releases](https://github.com/noqqe/serra/releases) Page and download the corresponding release for your platform.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
```
|
|
||||||
wget https://github.com/noqqe/serra/releases/download/3.10.0/serra_Darwin_x86_64.tar.gz
|
|
||||||
tar zxfv serra_Darwin_x86_64.tar.gz
|
|
||||||
./serra
|
|
||||||
```
|
|
||||||
|
|
||||||
## Upgrade Notes
|
|
||||||
|
|
||||||
### 2.x.x -> 3.x.x
|
|
||||||
|
|
||||||
No extra steps needed. Only new Webinterface and Foil support
|
|
||||||
|
|
||||||
### 1.5.3 -> 2.0.0
|
|
||||||
|
|
||||||
In this stage of the development of serra, I was breaking the original
|
|
||||||
database "schema" without migration.
|
|
||||||
|
|
||||||
Sadly you need to export the cards from the mongodb and import it again using `serra add ` commands
|
|
||||||
|
|
||||||
I wrote a little helper script in python to export all the cards in format set/number and generate some queries
|
|
||||||
|
|
||||||
```
|
|
||||||
python3 export.py > add_commands.sh
|
|
||||||
|
|
||||||
head add_commands.sh
|
|
||||||
./serra add 5ed/3 -c 1
|
|
||||||
./serra add mmq/2 -c 1
|
|
||||||
./serra add p02/4 -c 1
|
|
||||||
./serra add chr/44 -c 1
|
|
||||||
./serra add 4ed/291 -c 1
|
|
||||||
./serra add 4ed/292 -c 1
|
|
||||||
./serra add mir/2 -c 1
|
|
||||||
./serra add usg/231 -c 1
|
|
||||||
./serra add mir/155 -c 1
|
|
||||||
./serra add pcy/29 -c 2
|
|
||||||
|
|
||||||
<do the upgrade of serra (download new binary>
|
|
||||||
|
|
||||||
<delete the old mongodb or just empty it completly>
|
|
||||||
|
|
||||||
bash add_commands.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
from pymongo import MongoClient
|
|
||||||
import os
|
|
||||||
import pymongo
|
|
||||||
|
|
||||||
CONNECTION_STRING = os.getenv("MONGODB_URI")
|
|
||||||
client = MongoClient(CONNECTION_STRING+'/admin')
|
|
||||||
|
|
||||||
|
|
||||||
# Create a new collection
|
|
||||||
collection = client["serra"]["cards"]
|
|
||||||
|
|
||||||
cards=collection.find()
|
|
||||||
|
|
||||||
for c in cards:
|
|
||||||
print("./serra add %s/%s -c %s" % (c["set"], c["collectornumber"], c["serra_count"]))
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
#!/usr/bin/env fish
|
|
||||||
set SET $argv[1]
|
|
||||||
|
|
||||||
|
|
||||||
# export and remove colors
|
|
||||||
serra cards --set $SET --min-count 2 --sort value | gsed 's/\x1B[@A-Z\\\]^_]\|\x1B\[[0-9:;<=>?]*[-!"#$%&'"'"'()*+,.\/]*[][\\@A-Z^_`a-z{|}~]//g' > $SET
|
|
||||||
|
|
||||||
# edit
|
|
||||||
nvim $SET
|
|
||||||
|
|
||||||
# remove formatting
|
|
||||||
cat $SET | gsed 's/^\* //' | gsed 's/x.*(/ /' | gsed 's/).*//' | grep -v "Total Value" > {$SET}.txt
|
|
||||||
|
|
||||||
# delete everything from serra
|
|
||||||
for x in (cat {$SET}.txt) ; echo serra remove -c $x ; end
|
|
||||||
|
|
||||||
# cleanup
|
|
||||||
rm $SET
|
|
||||||
rm {$SET}.txt
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# This script replaces a normal card with a foil card I needed this because the
|
|
||||||
# tracking of foils was only added in version 3.5.0 of serra
|
|
||||||
|
|
||||||
# give set code as $1 like "dmr"
|
|
||||||
: ${1:?}
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
read -p "$1> " card
|
|
||||||
serra add --foil ${1}/${card}
|
|
||||||
serra remove ${1}/${card}
|
|
||||||
done
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
// Package main provides a typing test
|
// Package main provides a typing test
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/noqqe/serra/pkg/serra"
|
import "github.com/noqqe/serra/src/serra"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
serra.Execute()
|
serra.Execute()
|
||||||
128
src/serra/add.go
Normal file
128
src/serra/add.go
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
package serra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/chzyer/readline"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
addCmd.Flags().Int64VarP(&count, "count", "c", 1, "Amount of cards to add")
|
||||||
|
addCmd.Flags().BoolVarP(&unique, "unique", "u", false, "Only add card if not existent yet")
|
||||||
|
addCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Spin up interactive terminal")
|
||||||
|
addCmd.Flags().StringVarP(&set, "set", "s", "", "Filter by set code (usg/mmq/vow)")
|
||||||
|
addCmd.Flags().BoolVarP(&foil, "foil", "f", false, "Add foil variant of card")
|
||||||
|
rootCmd.AddCommand(addCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
var addCmd = &cobra.Command{
|
||||||
|
Aliases: []string{"a"},
|
||||||
|
Use: "add",
|
||||||
|
Short: "Add a card to your collection",
|
||||||
|
Long: "Adds a card from scryfall to your collection. Amount can be modified using flags",
|
||||||
|
SilenceErrors: true,
|
||||||
|
RunE: func(cmd *cobra.Command, cards []string) error {
|
||||||
|
|
||||||
|
if interactive {
|
||||||
|
addCardsInteractive(unique, set)
|
||||||
|
} else {
|
||||||
|
addCards(cards, unique, count)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func addCardsInteractive(unique bool, set string) {
|
||||||
|
|
||||||
|
if len(set) == 0 {
|
||||||
|
LogMessage("Error: --set must be given in interactive mode", "red")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
rl, err := readline.New(fmt.Sprintf("%s> ", set))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer rl.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
line, err := rl.Readline()
|
||||||
|
if err != nil { // io.EOF
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// construct card input for addCards
|
||||||
|
card := []string{}
|
||||||
|
card = append(card, fmt.Sprintf("%s/%s", set, strings.TrimSpace(line)))
|
||||||
|
|
||||||
|
addCards(card, unique, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func addCards(cards []string, unique bool, count int64) error {
|
||||||
|
client := storage_connect()
|
||||||
|
coll := &Collection{client.Database("serra").Collection("cards")}
|
||||||
|
defer storage_disconnect(client)
|
||||||
|
|
||||||
|
// Loop over different cards
|
||||||
|
for _, card := range cards {
|
||||||
|
|
||||||
|
// Check if card is already in collection
|
||||||
|
co, _ := coll.storage_find(bson.D{{"set", strings.Split(card, "/")[0]}, {"collectornumber", strings.Split(card, "/")[1]}}, bson.D{})
|
||||||
|
|
||||||
|
if len(co) >= 1 {
|
||||||
|
c := co[0]
|
||||||
|
|
||||||
|
if unique {
|
||||||
|
LogMessage(fmt.Sprintf("Not adding \"%s\" (%s, %.2f %s) to Collection because it already exists.", c.Name, c.Rarity, c.getValue(foil), getCurrency()), "red")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
modify_count_of_card(coll, &c, count, foil)
|
||||||
|
|
||||||
|
var total int64 = 0
|
||||||
|
if foil {
|
||||||
|
total = c.SerraCountFoil + count
|
||||||
|
} else {
|
||||||
|
total = c.SerraCount + count
|
||||||
|
}
|
||||||
|
// Give feedback of successfully added card
|
||||||
|
LogMessage(fmt.Sprintf("%dx \"%s\" (%s, %.2f %s) added to Collection.", total, c.Name, c.Rarity, c.getValue(foil), getCurrency()), "green")
|
||||||
|
|
||||||
|
// If card is not already in collection, fetching from scyfall
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Fetch card from scryfall
|
||||||
|
c, err := fetch_card(card)
|
||||||
|
if err != nil {
|
||||||
|
LogMessage(fmt.Sprintf("%v", err), "red")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write card to mongodb
|
||||||
|
var total int64 = 0
|
||||||
|
if foil {
|
||||||
|
c.SerraCountFoil = count
|
||||||
|
total = c.SerraCountFoil
|
||||||
|
} else {
|
||||||
|
c.SerraCount = count
|
||||||
|
total = c.SerraCount
|
||||||
|
}
|
||||||
|
err = coll.storage_add(c)
|
||||||
|
if err != nil {
|
||||||
|
LogMessage(fmt.Sprintf("%v", err), "red")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give feedback of successfully added card
|
||||||
|
LogMessage(fmt.Sprintf("%dx \"%s\" (%s, %.2f %s) added to Collection.", total, c.Name, c.Rarity, c.getValue(foil), getCurrency()), "green")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
storage_disconnect(client)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
158
src/serra/card.go
Normal file
158
src/serra/card.go
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
package serra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cardCmd.Flags().StringVarP(&rarity, "rarity", "r", "", "Filter by rarity of cards (mythic, rare, uncommon, common)")
|
||||||
|
cardCmd.Flags().StringVarP(&set, "set", "e", "", "Filter by set code (usg/mmq/vow)")
|
||||||
|
cardCmd.Flags().StringVarP(&sortby, "sort", "s", "name", "How to sort cards (value/number/name/added)")
|
||||||
|
cardCmd.Flags().StringVarP(&name, "name", "n", "", "Name of the card (regex compatible)")
|
||||||
|
cardCmd.Flags().StringVarP(&oracle, "oracle", "o", "", "Contains string in card text")
|
||||||
|
cardCmd.Flags().StringVarP(&cardType, "type", "t", "", "Contains string in card type line")
|
||||||
|
rootCmd.AddCommand(cardCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cardCmd = &cobra.Command{
|
||||||
|
Aliases: []string{"cards"},
|
||||||
|
Use: "card [card]",
|
||||||
|
Short: "Search & show cards from your collection",
|
||||||
|
Long: `Search and show cards from your collection.
|
||||||
|
If you directly put a card as an argument, it will be displayed
|
||||||
|
otherwise you'll get a list of cards as a search result.`,
|
||||||
|
SilenceErrors: true,
|
||||||
|
RunE: func(cmd *cobra.Command, cards []string) error {
|
||||||
|
if len(cards) == 0 {
|
||||||
|
card_list := Cards(rarity, set, sortby, name, oracle, cardType)
|
||||||
|
show_card_list(card_list)
|
||||||
|
} else {
|
||||||
|
ShowCard(cards)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShowCard(cardids []string) {
|
||||||
|
|
||||||
|
client := storage_connect()
|
||||||
|
coll := &Collection{client.Database("serra").Collection("cards")}
|
||||||
|
defer storage_disconnect(client)
|
||||||
|
|
||||||
|
for _, v := range cardids {
|
||||||
|
|
||||||
|
if len(strings.Split(v, "/")) < 2 || strings.Split(v, "/")[1] == "" {
|
||||||
|
LogMessage(fmt.Sprintf("Invalid card %s", v), "red")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cards, _ := coll.storage_find(bson.D{{"set", strings.Split(v, "/")[0]}, {"collectornumber", strings.Split(v, "/")[1]}}, bson.D{{"name", 1}})
|
||||||
|
|
||||||
|
for _, card := range cards {
|
||||||
|
show_card_details(&card)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Cards(rarity, set, sortby, name, oracle, cardType string) []Card {
|
||||||
|
|
||||||
|
client := storage_connect()
|
||||||
|
coll := &Collection{client.Database("serra").Collection("cards")}
|
||||||
|
defer storage_disconnect(client)
|
||||||
|
|
||||||
|
filter := bson.D{}
|
||||||
|
|
||||||
|
switch rarity {
|
||||||
|
case "uncommon":
|
||||||
|
filter = append(filter, bson.E{"rarity", "uncommon"})
|
||||||
|
case "common":
|
||||||
|
filter = append(filter, bson.E{"rarity", "common"})
|
||||||
|
case "rare":
|
||||||
|
filter = append(filter, bson.E{"rarity", "rare"})
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortStage bson.D
|
||||||
|
switch sortby {
|
||||||
|
case "value":
|
||||||
|
if getCurrency() == "EUR" {
|
||||||
|
sortStage = bson.D{{"prices.eur", 1}}
|
||||||
|
} else {
|
||||||
|
sortStage = bson.D{{"prices.usd", 1}}
|
||||||
|
}
|
||||||
|
case "number":
|
||||||
|
sortStage = bson.D{{"collectornumber", 1}}
|
||||||
|
case "name":
|
||||||
|
sortStage = bson.D{{"name", 1}}
|
||||||
|
case "added":
|
||||||
|
sortStage = bson.D{{"serra_created", 1}}
|
||||||
|
default:
|
||||||
|
sortStage = bson.D{{"name", 1}}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(set) > 0 {
|
||||||
|
filter = append(filter, bson.E{"set", set})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(name) > 0 {
|
||||||
|
filter = append(filter, bson.E{"name", bson.D{{"$regex", ".*" + name + ".*"}, {"$options", "i"}}})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(oracle) > 0 {
|
||||||
|
filter = append(filter, bson.E{"oracletext", bson.D{{"$regex", ".*" + oracle + ".*"}, {"$options", "i"}}})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cardType) > 0 {
|
||||||
|
filter = append(filter, bson.E{"typeline", bson.D{{"$regex", ".*" + cardType + ".*"}, {"$options", "i"}}})
|
||||||
|
}
|
||||||
|
|
||||||
|
cards, _ := coll.storage_find(filter, sortStage)
|
||||||
|
|
||||||
|
// This is needed because collectornumbers are strings (ie. "23a") but still we
|
||||||
|
// want it to be sorted numerically ... 1,2,3,10,11,100.
|
||||||
|
if sortby == "number" {
|
||||||
|
sort.Slice(cards, func(i, j int) bool {
|
||||||
|
return filterForDigits(cards[i].CollectorNumber) < filterForDigits(cards[j].CollectorNumber)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return cards
|
||||||
|
}
|
||||||
|
|
||||||
|
func show_card_list(cards []Card) {
|
||||||
|
|
||||||
|
var total float64
|
||||||
|
for _, card := range cards {
|
||||||
|
LogMessage(fmt.Sprintf("* %dx %s%s%s (%s/%s) %s%.2f %s%s", card.SerraCount+card.SerraCountFoil+card.SerraCountEtched, Purple, card.Name, Reset, card.Set, card.CollectorNumber, Yellow, card.getValue(false), getCurrency(), Reset), "normal")
|
||||||
|
total = total + card.getValue(false)*float64(card.SerraCount) + card.getValue(true)*float64(card.SerraCountFoil)
|
||||||
|
}
|
||||||
|
fmt.Printf("\nTotal Value: %s%.2f %s%s\n", Yellow, total, getCurrency(), Reset)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func show_card_details(card *Card) error {
|
||||||
|
fmt.Printf("%s%s%s (%s/%s)\n", Purple, card.Name, Reset, card.Set, card.CollectorNumber)
|
||||||
|
fmt.Printf("Added: %s\n", stringToTime(card.SerraCreated))
|
||||||
|
fmt.Printf("Count Normal: %dx\n", card.SerraCount)
|
||||||
|
if card.SerraCountFoil > 0 {
|
||||||
|
fmt.Printf("Count Foil: %dx\n", card.SerraCountFoil)
|
||||||
|
}
|
||||||
|
if card.SerraCountEtched > 0 {
|
||||||
|
fmt.Printf("Count Etched: %dx\n", card.SerraCountFoil)
|
||||||
|
}
|
||||||
|
fmt.Printf("Rarity: %s\n", card.Rarity)
|
||||||
|
fmt.Printf("Scryfall: %s\n", strings.Replace(card.ScryfallURI, "?utm_source=api", "", 1))
|
||||||
|
fmt.Printf("Current Value: %s%.2f %s%s\n", Yellow, card.getValue(false), getCurrency(), Reset)
|
||||||
|
if card.SerraCountFoil > 0 {
|
||||||
|
fmt.Printf("Foil Value: %s%.2f %s%s\n", Yellow, card.getValue(true), getCurrency(), Reset)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n%sHistory%s\n", Green, Reset)
|
||||||
|
print_price_history(card.SerraPrices, "* ", false)
|
||||||
|
fmt.Println()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
28
src/serra/env.go
Normal file
28
src/serra/env.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package serra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getMongoDBURI() string {
|
||||||
|
uri := os.Getenv("MONGODB_URI")
|
||||||
|
if uri == "" {
|
||||||
|
log.Fatal("You must set your 'MONGODB_URI' environmental variable. See\n\t https://docs.mongodb.com/drivers/go/current/usage-examples/#environment-variable")
|
||||||
|
}
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns configured human readable name for
|
||||||
|
// the configured currency of the user
|
||||||
|
func getCurrency() string {
|
||||||
|
switch os.Getenv("SERRA_CURRENCY") {
|
||||||
|
case "EUR":
|
||||||
|
return "EUR"
|
||||||
|
case "USD":
|
||||||
|
return "USD"
|
||||||
|
}
|
||||||
|
// default
|
||||||
|
LogMessage("Warning: You did not configure SERRA_CURRENCY. Assuming \"USD\"", "yellow")
|
||||||
|
return "USD"
|
||||||
|
}
|
||||||
@ -43,10 +43,10 @@ var flopsCmd = &cobra.Command{
|
|||||||
|
|
||||||
func Gains(limit float64, sort int) error {
|
func Gains(limit float64, sort int) error {
|
||||||
|
|
||||||
client := storageConnect()
|
client := storage_connect()
|
||||||
coll := &Collection{client.Database("serra").Collection("cards")}
|
coll := &Collection{client.Database("serra").Collection("cards")}
|
||||||
setcoll := &Collection{client.Database("serra").Collection("sets")}
|
setcoll := &Collection{client.Database("serra").Collection("sets")}
|
||||||
defer storageDisconnect(client)
|
defer storage_disconnect(client)
|
||||||
|
|
||||||
var old int
|
var old int
|
||||||
if sinceBeginning {
|
if sinceBeginning {
|
||||||
@ -57,11 +57,11 @@ func Gains(limit float64, sort int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currencyField := "$serra_prices.usd"
|
currencyField := "$serra_prices.usd"
|
||||||
if getCurrency() == EUR {
|
if getCurrency() == "EUR" {
|
||||||
currencyField = "$serra_prices.eur"
|
currencyField = "$serra_prices.eur"
|
||||||
}
|
}
|
||||||
|
|
||||||
raisePipeline := mongo.Pipeline{
|
raise_pipeline := mongo.Pipeline{
|
||||||
bson.D{{"$project",
|
bson.D{{"$project",
|
||||||
bson.D{
|
bson.D{
|
||||||
{"name", true},
|
{"name", true},
|
||||||
@ -109,9 +109,9 @@ func Gains(limit float64, sort int) error {
|
|||||||
bson.D{{"rate", sort}}}},
|
bson.D{{"rate", sort}}}},
|
||||||
bson.D{{"$limit", 20}},
|
bson.D{{"$limit", 20}},
|
||||||
}
|
}
|
||||||
raise, _ := coll.storageAggregate(raisePipeline)
|
raise, _ := coll.storage_aggregate(raise_pipeline)
|
||||||
|
|
||||||
sraisePipeline := mongo.Pipeline{
|
sraise_pipeline := mongo.Pipeline{
|
||||||
bson.D{{"$project",
|
bson.D{{"$project",
|
||||||
bson.D{
|
bson.D{
|
||||||
{"name", true},
|
{"name", true},
|
||||||
@ -157,25 +157,25 @@ func Gains(limit float64, sort int) error {
|
|||||||
bson.D{{"rate", sort}}}},
|
bson.D{{"rate", sort}}}},
|
||||||
bson.D{{"$limit", 10}},
|
bson.D{{"$limit", 10}},
|
||||||
}
|
}
|
||||||
sraise, _ := setcoll.storageAggregate(sraisePipeline)
|
sraise, _ := setcoll.storage_aggregate(sraise_pipeline)
|
||||||
|
|
||||||
// percentage coloring
|
// percentage coloring
|
||||||
var pColor string
|
var p_color string
|
||||||
if sort == 1 {
|
if sort == 1 {
|
||||||
pColor = Red
|
p_color = Red
|
||||||
} else {
|
} else {
|
||||||
pColor = Green
|
p_color = Green
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%sCards%s\n", Purple, Reset)
|
fmt.Printf("%sCards%s\n", Purple, Reset)
|
||||||
// print each card
|
// print each card
|
||||||
for _, e := range raise {
|
for _, e := range raise {
|
||||||
fmt.Printf("%s%+.0f%%%s %s %s(%s/%s)%s (%.2f->%s%.2f%s%s) \n", pColor, e["rate"], Reset, e["name"], Yellow, e["set"], e["collectornumber"], Reset, e["old"], Green, e["current"], getCurrency(), Reset)
|
fmt.Printf("%s%+.0f%%%s %s %s(%s/%s)%s (%.2f->%s%.2f %s%s) \n", p_color, e["rate"], Reset, e["name"], Yellow, e["set"], e["collectornumber"], Reset, e["old"], Green, e["current"], getCurrency(), Reset)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n%sSets%s\n", Purple, Reset)
|
fmt.Printf("\n%sSets%s\n", Purple, Reset)
|
||||||
for _, e := range sraise {
|
for _, e := range sraise {
|
||||||
fmt.Printf("%s%+.0f%%%s %s %s(%s)%s (%.2f->%s%.2f%s%s) \n", pColor, e["rate"], Reset, e["name"], Yellow, e["code"], Reset, e["old"], Green, e["current"], getCurrency(), Reset)
|
fmt.Printf("%s%+.0f%%%s %s %s(%s)%s (%.2f->%s%.2f %s%s) \n", p_color, e["rate"], Reset, e["name"], Yellow, e["code"], Reset, e["old"], Green, e["current"], getCurrency(), Reset)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
227
src/serra/helpers.go
Normal file
227
src/serra/helpers.go
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
package serra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Rarities struct {
|
||||||
|
Rares, Uncommons, Commons, Mythics float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func modify_count_of_card(coll *Collection, c *Card, amount int64, foil bool) error {
|
||||||
|
|
||||||
|
// find already existing card
|
||||||
|
sort := bson.D{{"_id", 1}}
|
||||||
|
search_filter := bson.D{{"_id", c.ID}}
|
||||||
|
stored_cards, err := coll.storage_find(search_filter, sort)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stored_card := stored_cards[0]
|
||||||
|
|
||||||
|
// update card amount
|
||||||
|
update_filter := bson.M{"_id": bson.M{"$eq": c.ID}}
|
||||||
|
var update bson.M
|
||||||
|
if foil {
|
||||||
|
update = bson.M{
|
||||||
|
"$set": bson.M{"serra_count_foil": stored_card.SerraCountFoil + amount},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
update = bson.M{
|
||||||
|
"$set": bson.M{"serra_count": stored_card.SerraCount + amount},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coll.storage_update(update_filter, update)
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
if foil {
|
||||||
|
total = stored_card.SerraCountFoil + amount
|
||||||
|
} else {
|
||||||
|
total = stored_card.SerraCount + amount
|
||||||
|
}
|
||||||
|
LogMessage(fmt.Sprintf("Updating Card \"%s\" amount to %d", stored_card.Name, total), "purple")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func find_card_by_setcollectornumber(coll *Collection, setcode string, collectornumber string) (*Card, error) {
|
||||||
|
|
||||||
|
sort := bson.D{{"_id", 1}}
|
||||||
|
search_filter := bson.D{{"set", setcode}, {"collectornumber", collectornumber}}
|
||||||
|
stored_cards, err := coll.storage_find(search_filter, sort)
|
||||||
|
if err != nil {
|
||||||
|
return &Card{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(stored_cards) < 1 {
|
||||||
|
return &Card{}, errors.New("Card not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &stored_cards[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringToTime(s primitive.DateTime) string {
|
||||||
|
return time.UnixMilli(int64(s)).Format("2006-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
// missing compares two slices and returns slice of differences
|
||||||
|
func missing(a, b []string) []string {
|
||||||
|
type void struct{}
|
||||||
|
// create map with length of the 'a' slice
|
||||||
|
ma := make(map[string]void, len(a))
|
||||||
|
diffs := []string{}
|
||||||
|
// Convert first slice to map with empty struct (0 bytes)
|
||||||
|
for _, ka := range a {
|
||||||
|
ma[ka] = void{}
|
||||||
|
}
|
||||||
|
// find missing values in a
|
||||||
|
for _, kb := range b {
|
||||||
|
if _, ok := ma[kb]; !ok {
|
||||||
|
diffs = append(diffs, kb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return diffs
|
||||||
|
}
|
||||||
|
|
||||||
|
func find_set_by_code(coll *Collection, setcode string) (*Set, error) {
|
||||||
|
|
||||||
|
stored_sets, err := coll.storage_find_set(bson.D{{"code", setcode}}, bson.D{{"_id", 1}})
|
||||||
|
if err != nil {
|
||||||
|
return &Set{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(stored_sets) < 1 {
|
||||||
|
return &Set{}, errors.New("Set not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &stored_sets[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convert_mana_symbols(sym []interface{}) string {
|
||||||
|
var mana string
|
||||||
|
|
||||||
|
if len(sym) == 0 {
|
||||||
|
// mana = mana + "\U0001F6AB" //probibited sign for lands
|
||||||
|
mana = mana + "None" //probibited sign for lands
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range sym {
|
||||||
|
switch v {
|
||||||
|
case "B":
|
||||||
|
mana = mana + "Black" //black
|
||||||
|
//mana = mana + "\U000026AB" //black
|
||||||
|
case "R":
|
||||||
|
mana = mana + "Red" //red
|
||||||
|
// mana = mana + "\U0001F534" //red
|
||||||
|
case "G":
|
||||||
|
mana = mana + "Green" //green
|
||||||
|
// mana = mana + "\U0001F7E2" //green
|
||||||
|
case "U":
|
||||||
|
mana = mana + "Blue" //blue
|
||||||
|
//mana = mana + "\U0001F535" //blue
|
||||||
|
case "W":
|
||||||
|
mana = mana + "White" //white
|
||||||
|
// mana = mana + "\U000026AA" //white
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mana
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func convert_rarities(rar []primitive.M) Rarities {
|
||||||
|
|
||||||
|
// this is maybe the ugliest way someone could choose to verify, if a rarity type is missing
|
||||||
|
// [
|
||||||
|
// { _id: { rarity: 'common' }, count: 20 },
|
||||||
|
// { _id: { rarity: 'uncommon' }, count: 2 }
|
||||||
|
// ]
|
||||||
|
// if a result like this is there, 1 rarity type "rare" is not in the array. and needs to be
|
||||||
|
// initialized with 0, otherwise we get a panic
|
||||||
|
|
||||||
|
var ri Rarities
|
||||||
|
for _, r := range rar {
|
||||||
|
switch r["_id"] {
|
||||||
|
case "rare":
|
||||||
|
ri.Rares = r["count"].(float64)
|
||||||
|
case "uncommon":
|
||||||
|
ri.Uncommons = r["count"].(float64)
|
||||||
|
case "common":
|
||||||
|
ri.Commons = r["count"].(float64)
|
||||||
|
case "mythic":
|
||||||
|
ri.Mythics = r["count"].(float64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ri
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func print_price_history(prices []PriceEntry, prefix string, total bool) {
|
||||||
|
|
||||||
|
var before float64
|
||||||
|
for _, e := range prices {
|
||||||
|
|
||||||
|
var value float64
|
||||||
|
if total {
|
||||||
|
value = e.Usd + e.UsdFoil + e.UsdEtched
|
||||||
|
if getCurrency() == "EUR" {
|
||||||
|
value = e.Eur + e.EurFoil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = e.Usd
|
||||||
|
if getCurrency() == "EUR" {
|
||||||
|
value = e.Eur
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if value > before && before != 0 {
|
||||||
|
fmt.Printf("%s%s%s %.2f %s%s (%+.2f%%, %+.2f %s)\n", prefix, stringToTime(e.Date), Green, value, getCurrency(), Reset, (value/before*100)-100, value-before, getCurrency())
|
||||||
|
} else if value < before {
|
||||||
|
fmt.Printf("%s%s%s %.2f %s%s (%+.2f%%, %+.2f %s)\n", prefix, stringToTime(e.Date), Red, value, getCurrency(), Reset, (value/before*100)-100, value-before, getCurrency())
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s%s %.2f %s%s\n", prefix, stringToTime(e.Date), value, getCurrency(), Reset)
|
||||||
|
}
|
||||||
|
before = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterForDigits(str string) int {
|
||||||
|
var numStr string
|
||||||
|
for _, c := range str {
|
||||||
|
if unicode.IsDigit(c) {
|
||||||
|
numStr += string(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s, _ := strconv.Atoi(numStr)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFloat64(unknown interface{}) (float64, error) {
|
||||||
|
switch i := unknown.(type) {
|
||||||
|
case float64:
|
||||||
|
return i, nil
|
||||||
|
case float32:
|
||||||
|
return float64(i), nil
|
||||||
|
case int64:
|
||||||
|
return float64(i), nil
|
||||||
|
case int32:
|
||||||
|
return float64(i), nil
|
||||||
|
case int:
|
||||||
|
return float64(i), nil
|
||||||
|
case uint64:
|
||||||
|
return float64(i), nil
|
||||||
|
case uint32:
|
||||||
|
return float64(i), nil
|
||||||
|
case uint:
|
||||||
|
return float64(i), nil
|
||||||
|
default:
|
||||||
|
return math.NaN(), errors.New("Non-numeric type could not be converted to float")
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/serra/missing.go
Normal file
65
src/serra/missing.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package serra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(missingCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
var missingCmd = &cobra.Command{
|
||||||
|
Aliases: []string{"m"},
|
||||||
|
Use: "missing <set>",
|
||||||
|
Short: "Display missing cards from a set",
|
||||||
|
Long: `In case you are a set collector, you can generate a list of
|
||||||
|
cards you dont own (yet) :)`,
|
||||||
|
SilenceErrors: true,
|
||||||
|
RunE: func(cmd *cobra.Command, setname []string) error {
|
||||||
|
|
||||||
|
client := storage_connect()
|
||||||
|
coll := &Collection{client.Database("serra").Collection("cards")}
|
||||||
|
defer storage_disconnect(client)
|
||||||
|
|
||||||
|
// fetch all cards in set
|
||||||
|
cards, err := coll.storage_find(bson.D{{"set", setname[0]}}, bson.D{{"collectornumber", 1}})
|
||||||
|
if (err != nil) || len(cards) == 0 {
|
||||||
|
LogMessage(fmt.Sprintf("Error: Set %s not found or no card in your collection.", setname[0]), "red")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch set informations
|
||||||
|
setcoll := &Collection{client.Database("serra").Collection("sets")}
|
||||||
|
sets, _ := setcoll.storage_find_set(bson.D{{"code", setname[0]}}, bson.D{{"_id", 1}})
|
||||||
|
set := sets[0]
|
||||||
|
|
||||||
|
LogMessage(fmt.Sprintf("Missing cards in %s", sets[0].Name), "green")
|
||||||
|
|
||||||
|
// generate set with all setnumbers
|
||||||
|
var complete_set []string
|
||||||
|
var i int64
|
||||||
|
for i = 1; i <= set.CardCount; i++ {
|
||||||
|
complete_set = append(complete_set, strconv.FormatInt(i, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate over all cards in collection
|
||||||
|
var in_collection []string
|
||||||
|
for _, c := range cards {
|
||||||
|
in_collection = append(in_collection, c.CollectorNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
misses := missing(in_collection, complete_set)
|
||||||
|
for _, m := range misses {
|
||||||
|
ncard, err := fetch_card(fmt.Sprintf("%s/%s", setname[0], m))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Printf("%.02f %s\t%s (%s)\n", ncard.getValue(false), getCurrency(), ncard.Name, ncard.SetName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ package serra
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/chzyer/readline"
|
"github.com/chzyer/readline"
|
||||||
@ -26,7 +27,7 @@ var removeCmd = &cobra.Command{
|
|||||||
RunE: func(cmd *cobra.Command, cards []string) error {
|
RunE: func(cmd *cobra.Command, cards []string) error {
|
||||||
|
|
||||||
if interactive {
|
if interactive {
|
||||||
removeCardsInteractive(set)
|
removeCardsInteractive(unique, set)
|
||||||
} else {
|
} else {
|
||||||
removeCards(cards, count)
|
removeCards(cards, count)
|
||||||
}
|
}
|
||||||
@ -34,11 +35,11 @@ var removeCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeCardsInteractive(set string) {
|
func removeCardsInteractive(unique bool, set string) {
|
||||||
l := Logger()
|
|
||||||
|
|
||||||
if len(set) == 0 {
|
if len(set) == 0 {
|
||||||
l.Fatal("Option --set must be given in interactive mode")
|
LogMessage("Error: --set must be given in interactive mode", "red")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
rl, err := readline.New(fmt.Sprintf("%s> ", set))
|
rl, err := readline.New(fmt.Sprintf("%s> ", set))
|
||||||
@ -63,57 +64,37 @@ func removeCardsInteractive(set string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func removeCards(cards []string, count int64) error {
|
func removeCards(cards []string, count int64) error {
|
||||||
// Connect to the DB & load the collection
|
|
||||||
client := storageConnect()
|
client := storage_connect()
|
||||||
coll := &Collection{client.Database("serra").Collection("cards")}
|
coll := &Collection{client.Database("serra").Collection("cards")}
|
||||||
l := Logger()
|
defer storage_disconnect(client)
|
||||||
defer storageDisconnect(client)
|
|
||||||
|
|
||||||
// Loop over different cards
|
// Loop over different cards
|
||||||
for _, card := range cards {
|
for _, card := range cards {
|
||||||
|
|
||||||
if !strings.Contains(card, "/") {
|
|
||||||
l.Errorf("Invalid card format %s. Needs to be set/collector number i.e. \"usg/13\"", card)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract collector number and set name from input & remove leading zeros
|
|
||||||
collectorNumber := strings.TrimLeft(strings.Split(card, "/")[1], "0")
|
|
||||||
setName := strings.Split(card, "/")[0]
|
|
||||||
|
|
||||||
if collectorNumber == "" {
|
|
||||||
l.Errorf("Invalid card format %s. Needs to be set/collector number i.e. \"usg/13\"", card)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch card from scryfall
|
// Fetch card from scryfall
|
||||||
c, err := findCardByCollectorNumber(coll, setName, collectorNumber)
|
c, err := find_card_by_setcollectornumber(coll, strings.Split(card, "/")[0], strings.Split(card, "/")[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Error(err)
|
LogMessage(fmt.Sprintf("%v", err), "red")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if foil && c.SerraCountFoil < 1 {
|
if foil && c.SerraCountFoil < 1 {
|
||||||
l.Errorf("No foil \"%s\" in the collection", c.Name)
|
LogMessage(fmt.Sprintf("Error: No Foil \"%s\" in the Collection.", c.Name), "red")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !foil && c.SerraCount < 1 {
|
if !foil && c.SerraCount < 1 {
|
||||||
l.Errorf("No normal \"%s\" in the collection", c.Name)
|
LogMessage(fmt.Sprintf("Error: No Non-Foil \"%s\" in the Collection.", c.Name), "red")
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !askConfirmation(c) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if foil && c.SerraCountFoil == 1 && c.SerraCount == 0 || !foil && c.SerraCount == 1 && c.SerraCountFoil == 0 {
|
if foil && c.SerraCountFoil == 1 && c.SerraCount == 0 || !foil && c.SerraCount == 1 && c.SerraCountFoil == 0 {
|
||||||
coll.storageRemove(bson.M{"_id": c.ID})
|
coll.storage_remove(bson.M{"_id": c.ID})
|
||||||
l.Infof("\"%s\" (%.2f%s) removed", c.Name, c.getValue(foil), getCurrency())
|
LogMessage(fmt.Sprintf("\"%s\" (%.2f %s) removed from the Collection.", c.Name, c.getValue(foil), getCurrency()), "green")
|
||||||
} else {
|
} else {
|
||||||
modifyCardCount(coll, c, -count, foil)
|
modify_count_of_card(coll, c, -1, foil)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
39
src/serra/root.go
Normal file
39
src/serra/root.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package serra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Version = "unknown"
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
var limit float64
|
||||||
|
var interactive bool
|
||||||
|
var name string
|
||||||
|
var oracle string
|
||||||
|
var rarity string
|
||||||
|
var set string
|
||||||
|
var sinceBeginning bool
|
||||||
|
var sinceLastUpdate bool
|
||||||
|
var sortby string
|
||||||
|
var cardType string
|
||||||
|
var unique bool
|
||||||
|
var foil bool
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Version: Version,
|
||||||
|
Long: `serra - Magic: The Gathering Collection Tracker`,
|
||||||
|
Use: "serra",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
SilenceErrors: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func Execute() {
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,14 +2,13 @@ package serra
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
@ -17,24 +16,19 @@ import (
|
|||||||
|
|
||||||
type Card struct {
|
type Card struct {
|
||||||
// Added by Serra
|
// Added by Serra
|
||||||
SerraCount int64 `bson:"serra_count"`
|
SerraCount int64 `bson:"serra_count"`
|
||||||
SerraCountFoil int64 `bson:"serra_count_foil"`
|
SerraCountFoil int64 `bson:"serra_count_foil"`
|
||||||
SerraCountEtched int64 `bson:"serra_count_etched"`
|
SerraCountEtched int64 `bson:"serra_count_etched"`
|
||||||
SerraCountDeck int64 `bson:"serra_count_deck"`
|
SerraPrices []PriceEntry `bson:"serra_prices"`
|
||||||
SerraCountFoilDeck int64 `bson:"serra_count_foil_deck"`
|
SerraCreated primitive.DateTime `bson:"serra_created"`
|
||||||
SerraCountEtchedDeck int64 `bson:"serra_count_etched_deck"`
|
SerraUpdated primitive.DateTime `bson:"serra_updated"`
|
||||||
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"`
|
Artist string `json:"artist"`
|
||||||
ArtistIds []string `json:"artist_ids"`
|
ArtistIds []string `json:"artist_ids"`
|
||||||
Booster bool `json:"booster"`
|
Booster bool `json:"booster"`
|
||||||
BorderColor string `json:"border_color"`
|
BorderColor string `json:"border_color"`
|
||||||
CardBackID string `json:"card_back_id"`
|
CardBackID string `json:"card_back_id"`
|
||||||
CardmarketID float64 `json:"cardmarket_id"`
|
Cmc int64 `json:"cmc"`
|
||||||
Cmc float64 `json:"cmc"`
|
|
||||||
CollectorNumber string `json:"collector_number"`
|
CollectorNumber string `json:"collector_number"`
|
||||||
ColorIdentity []string `json:"color_identity"`
|
ColorIdentity []string `json:"color_identity"`
|
||||||
Colors []string `json:"colors"`
|
Colors []string `json:"colors"`
|
||||||
@ -106,139 +100,37 @@ type Card struct {
|
|||||||
TcgplayerInfiniteArticles string `json:"tcgplayer_infinite_articles"`
|
TcgplayerInfiniteArticles string `json:"tcgplayer_infinite_articles"`
|
||||||
TcgplayerInfiniteDecks string `json:"tcgplayer_infinite_decks"`
|
TcgplayerInfiniteDecks string `json:"tcgplayer_infinite_decks"`
|
||||||
} `json:"related_uris"`
|
} `json:"related_uris"`
|
||||||
ReleasedAt string `json:"released_at"`
|
ReleasedAt string `json:"released_at"`
|
||||||
Reprint bool `json:"reprint"`
|
Reprint bool `json:"reprint"`
|
||||||
Reserved bool `json:"reserved"`
|
Reserved bool `json:"reserved"`
|
||||||
RulingsURI string `json:"rulings_uri"`
|
RulingsURI string `json:"rulings_uri"`
|
||||||
ScryfallSetURI string `json:"scryfall_set_uri"`
|
ScryfallSetURI string `json:"scryfall_set_uri"`
|
||||||
ScryfallURI string `json:"scryfall_uri"`
|
ScryfallURI string `json:"scryfall_uri"`
|
||||||
Set string `json:"set"`
|
Set string `json:"set"`
|
||||||
SetID string `json:"set_id"`
|
SetID string `json:"set_id"`
|
||||||
SetName string `json:"set_name"`
|
SetName string `json:"set_name"`
|
||||||
SetSearchURI string `json:"set_search_uri"`
|
SetSearchURI string `json:"set_search_uri"`
|
||||||
SetType string `json:"set_type"`
|
SetType string `json:"set_type"`
|
||||||
SetURI string `json:"set_uri"`
|
SetURI string `json:"set_uri"`
|
||||||
StorySpotlight bool `json:"story_spotlight"`
|
StorySpotlight bool `json:"story_spotlight"`
|
||||||
Textless bool `json:"textless"`
|
Textless bool `json:"textless"`
|
||||||
TCGPlayerID float64 `json:"tcgplayer_id"`
|
TypeLine string `json:"type_line"`
|
||||||
TypeLine string `json:"type_line"`
|
URI string `json:"uri"`
|
||||||
URI string `json:"uri"`
|
Variation bool `json:"variation"`
|
||||||
Variation bool `json:"variation"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BulkIndex struct {
|
// Getter for currency specific value
|
||||||
Object string `json:"object"`
|
func (c Card) getValue(foil bool) float64 {
|
||||||
HasMore bool `json:"has_more"`
|
if getCurrency() == "EUR" {
|
||||||
Data []struct {
|
if foil {
|
||||||
Object string `json:"object"`
|
return c.Prices.EurFoil
|
||||||
ID string `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
URI string `json:"uri"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Size int `json:"size"`
|
|
||||||
DownloadURI string `json:"download_uri"`
|
|
||||||
ContentType string `json:"content_type"`
|
|
||||||
ContentEncoding string `json:"content_encoding"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchBulkDownloadURL() (string, error) {
|
|
||||||
url := "https://api.scryfall.com/bulk-data"
|
|
||||||
downloadURL := ""
|
|
||||||
|
|
||||||
// Make an HTTP GET request
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error fetching data: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// 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 c.Prices.Eur
|
||||||
}
|
}
|
||||||
|
if foil {
|
||||||
return downloadURL, nil
|
return c.Prices.UsdFoil
|
||||||
}
|
|
||||||
|
|
||||||
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
|
return c.Prices.Usd
|
||||||
|
|
||||||
// Create a temporary file in the temporary directory
|
|
||||||
tempFile, err := os.CreateTemp(tempDir, "downloaded-*.json") // Adjust the extension if necessary
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error creating temporary file: %v", err)
|
|
||||||
}
|
|
||||||
// defer tempFile.Close() // Ensure we close the file when we're done
|
|
||||||
|
|
||||||
// Download the file
|
|
||||||
resp, err := http.Get(downloadURL)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error downloading file: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close() // Make sure to close the response body
|
|
||||||
|
|
||||||
// Check for a successful response
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
log.Fatalf("Error: received status code %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the response body to the temporary file
|
|
||||||
_, err = io.Copy(tempFile, resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error saving file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tempFile.Name(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadBulkFile(bulkFilePath string) ([]Card, error) {
|
|
||||||
|
|
||||||
var cards []Card
|
|
||||||
fileBytes, _ := os.ReadFile(bulkFilePath)
|
|
||||||
defer os.Remove(bulkFilePath)
|
|
||||||
|
|
||||||
err := json.Unmarshal(fileBytes, &cards)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error unmarshalling bulk file:", err)
|
|
||||||
return cards, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return cards, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCardFromBulk(cards []Card, setName, collectorNumber string) (*Card, error) {
|
|
||||||
var foundCard Card
|
|
||||||
for _, v := range cards {
|
|
||||||
if v.CollectorNumber == collectorNumber && v.Set == setName {
|
|
||||||
foundCard = v
|
|
||||||
return &foundCard, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &Card{}, fmt.Errorf("Card %s/%s not found in bulk data", setName, collectorNumber)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PriceEntry struct {
|
type PriceEntry struct {
|
||||||
@ -278,35 +170,29 @@ type Set struct {
|
|||||||
URI string `json:"uri"`
|
URI string `json:"uri"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getter for currency specific value
|
func fetch_card(path string) (*Card, error) {
|
||||||
func (c Card) getValue(foil bool) float64 {
|
|
||||||
if getCurrency() == EUR {
|
|
||||||
if foil {
|
|
||||||
return c.Prices.EurFoil
|
|
||||||
}
|
|
||||||
return c.Prices.Eur
|
|
||||||
}
|
|
||||||
if foil {
|
|
||||||
return c.Prices.UsdFoil
|
|
||||||
}
|
|
||||||
return c.Prices.Usd
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchCard(setName, collectorNumber string) (*Card, error) {
|
if !strings.Contains(path, "/") {
|
||||||
resp, err := http.Get(fmt.Sprintf("https://api.scryfall.com/cards/%s/%s/", setName, collectorNumber))
|
err := errors.New(fmt.Sprintf("Card must follow format <set>/<number>, for example: ath/15"))
|
||||||
|
return &Card{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO better URL Building...
|
||||||
|
resp, err := http.Get(fmt.Sprintf("https://api.scryfall.com/cards/%s/", path))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
return &Card{}, err
|
return &Card{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
return &Card{}, fmt.Errorf("Card %s/%s not found", setName, collectorNumber)
|
err := errors.New(fmt.Sprintf("Error: %s not found", path))
|
||||||
|
return &Card{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
//we read the response body on the line below.
|
//We Read the response body on the line below.
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("%s", err)
|
log.Fatalln(err)
|
||||||
return &Card{}, err
|
return &Card{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,9 +200,6 @@ func fetchCard(setName, collectorNumber string) (*Card, error) {
|
|||||||
decoder := json.NewDecoder(r)
|
decoder := json.NewDecoder(r)
|
||||||
val := &Card{}
|
val := &Card{}
|
||||||
err = decoder.Decode(val)
|
err = decoder.Decode(val)
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("%s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set created Time
|
// Set created Time
|
||||||
val.SerraCreated = primitive.NewDateTimeFromTime(time.Now())
|
val.SerraCreated = primitive.NewDateTimeFromTime(time.Now())
|
||||||
@ -325,36 +208,24 @@ func fetchCard(setName, collectorNumber string) (*Card, error) {
|
|||||||
val.Prices.Date = primitive.NewDateTimeFromTime(time.Now())
|
val.Prices.Date = primitive.NewDateTimeFromTime(time.Now())
|
||||||
val.SerraPrices = append(val.SerraPrices, val.Prices)
|
val.SerraPrices = append(val.SerraPrices, val.Prices)
|
||||||
|
|
||||||
imgResp, imgErr := http.Get(val.ImageUris.Png)
|
|
||||||
if imgErr != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
return &Card{}, err
|
|
||||||
}
|
|
||||||
bytes, readErr := io.ReadAll(imgResp.Body)
|
|
||||||
if readErr != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
return &Card{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
val.SerraImage64 = base64.StdEncoding.EncodeToString(bytes)
|
|
||||||
|
|
||||||
return val, nil
|
return val, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchSets() (*SetList, error) {
|
func fetch_sets() (*SetList, error) {
|
||||||
// TODO: better URL Building...
|
// TODO better URL Building...
|
||||||
resp, err := http.Get("https://api.scryfall.com/sets")
|
resp, err := http.Get(fmt.Sprintf("https://api.scryfall.com/sets"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
return &SetList{}, err
|
return &SetList{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
return &SetList{}, fmt.Errorf("/sets not found")
|
err := errors.New(fmt.Sprintf("Error: /sets not found"))
|
||||||
|
return &SetList{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
//We Read the response body on the line below.
|
//We Read the response body on the line below.
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
return &SetList{}, err
|
return &SetList{}, err
|
||||||
@ -363,11 +234,7 @@ func fetchSets() (*SetList, error) {
|
|||||||
r := bytes.NewReader(body)
|
r := bytes.NewReader(body)
|
||||||
decoder := json.NewDecoder(r)
|
decoder := json.NewDecoder(r)
|
||||||
val := &SetList{}
|
val := &SetList{}
|
||||||
|
|
||||||
err = decoder.Decode(val)
|
err = decoder.Decode(val)
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return val, nil
|
return val, nil
|
||||||
}
|
}
|
||||||
@ -25,7 +25,7 @@ otherwise you'll get a list of sets as a search result.`,
|
|||||||
RunE: func(cmd *cobra.Command, set []string) error {
|
RunE: func(cmd *cobra.Command, set []string) error {
|
||||||
if len(set) == 0 {
|
if len(set) == 0 {
|
||||||
setList := Sets(sortby)
|
setList := Sets(sortby)
|
||||||
showSetList(setList)
|
show_set_list(setList)
|
||||||
} else {
|
} else {
|
||||||
ShowSet(set[0])
|
ShowSet(set[0])
|
||||||
}
|
}
|
||||||
@ -35,9 +35,9 @@ otherwise you'll get a list of sets as a search result.`,
|
|||||||
|
|
||||||
func Sets(sort string) []primitive.M {
|
func Sets(sort string) []primitive.M {
|
||||||
|
|
||||||
client := storageConnect()
|
client := storage_connect()
|
||||||
coll := &Collection{client.Database("serra").Collection("cards")}
|
coll := &Collection{client.Database("serra").Collection("cards")}
|
||||||
defer storageDisconnect(client)
|
defer storage_disconnect(client)
|
||||||
|
|
||||||
groupStage := bson.D{
|
groupStage := bson.D{
|
||||||
{"$group", bson.D{
|
{"$group", bson.D{
|
||||||
@ -65,46 +65,45 @@ func Sets(sort string) []primitive.M {
|
|||||||
}}}
|
}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
sets, _ := coll.storageAggregate(mongo.Pipeline{groupStage, sortStage})
|
sets, _ := coll.storage_aggregate(mongo.Pipeline{groupStage, sortStage})
|
||||||
return sets
|
return sets
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func showSetList(sets []primitive.M) {
|
func show_set_list(sets []primitive.M) {
|
||||||
|
|
||||||
client := storageConnect()
|
client := storage_connect()
|
||||||
setscoll := &Collection{client.Database("serra").Collection("sets")}
|
setscoll := &Collection{client.Database("serra").Collection("sets")}
|
||||||
|
|
||||||
for _, set := range sets {
|
for _, set := range sets {
|
||||||
setobj, _ := findSetByCode(setscoll, set["code"].(string))
|
setobj, _ := find_set_by_code(setscoll, set["code"].(string))
|
||||||
fmt.Printf("* %s %s%s%s (%s%s%s)\n", set["release"].(string)[0:4], Purple, set["_id"], Reset, Cyan, set["code"], Reset)
|
fmt.Printf("* %s %s%s%s (%s%s%s)\n", set["release"].(string)[0:4], Purple, set["_id"], Reset, Cyan, set["code"], Reset)
|
||||||
fmt.Printf(" Cards: %s%d/%d%s Total: %.0f \n", Yellow, set["unique"], setobj.CardCount, Reset, set["count"])
|
fmt.Printf(" Cards: %s%d/%d%s Total: %.0f \n", Yellow, set["unique"], setobj.CardCount, Reset, set["count"])
|
||||||
fmt.Printf(" Value: %s%.2f%s%s\n", Pink, set["value"], getCurrency(), Reset)
|
fmt.Printf(" Value: %s%.2f %s%s\n", Pink, set["value"], getCurrency(), Reset)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ShowSet(setname string) error {
|
func ShowSet(setname string) error {
|
||||||
|
|
||||||
client := storageConnect()
|
client := storage_connect()
|
||||||
coll := &Collection{client.Database("serra").Collection("cards")}
|
coll := &Collection{client.Database("serra").Collection("cards")}
|
||||||
l := Logger()
|
defer storage_disconnect(client)
|
||||||
defer storageDisconnect(client)
|
|
||||||
|
|
||||||
// fetch all cards in set ordered by currently used currency
|
// fetch all cards in set ordered by currently used currency
|
||||||
cardSortCurrency := bson.D{{"prices.usd", -1}}
|
cardSortCurrency := bson.D{{"prices.usd", -1}}
|
||||||
if getCurrency() == EUR {
|
if getCurrency() == "EUR" {
|
||||||
cardSortCurrency = bson.D{{"prices.eur", -1}}
|
cardSortCurrency = bson.D{{"prices.eur", -1}}
|
||||||
}
|
}
|
||||||
cards, err := coll.storageFind(bson.D{{"set", setname}}, cardSortCurrency, 0, 0)
|
cards, err := coll.storage_find(bson.D{{"set", setname}}, cardSortCurrency)
|
||||||
if (err != nil) || len(cards) == 0 {
|
if (err != nil) || len(cards) == 0 {
|
||||||
l.Errorf("Set %s not found or no card in your collection.", setname)
|
LogMessage(fmt.Sprintf("Error: Set %s not found or no card in your collection.", setname), "red")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch set informations
|
// fetch set informations
|
||||||
setcoll := &Collection{client.Database("serra").Collection("sets")}
|
setcoll := &Collection{client.Database("serra").Collection("sets")}
|
||||||
sets, _ := setcoll.storageFindSet(bson.D{{"code", setname}}, bson.D{{"_id", 1}})
|
sets, _ := setcoll.storage_find_set(bson.D{{"code", setname}}, bson.D{{"_id", 1}})
|
||||||
|
|
||||||
// set values
|
// set values
|
||||||
matchStage := bson.D{
|
matchStage := bson.D{
|
||||||
@ -118,10 +117,9 @@ func ShowSet(setname string) error {
|
|||||||
{"value", bson.D{{"$sum", bson.D{{"$multiply", bson.A{getCurrencyField(false), "$serra_count"}}}}}},
|
{"value", bson.D{{"$sum", bson.D{{"$multiply", bson.A{getCurrencyField(false), "$serra_count"}}}}}},
|
||||||
{"value_foil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{getCurrencyField(true), "$serra_count_foil"}}}}}},
|
{"value_foil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{getCurrencyField(true), "$serra_count_foil"}}}}}},
|
||||||
{"count", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count"}}}}}},
|
{"count", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count"}}}}}},
|
||||||
{"count_foil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count_foil"}}}}}},
|
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
stats, _ := coll.storageAggregate(mongo.Pipeline{matchStage, groupStage})
|
stats, _ := coll.storage_aggregate(mongo.Pipeline{matchStage, groupStage})
|
||||||
|
|
||||||
// set rarities
|
// set rarities
|
||||||
matchStage = bson.D{
|
matchStage = bson.D{
|
||||||
@ -139,44 +137,32 @@ func ShowSet(setname string) error {
|
|||||||
{"$sort", bson.D{
|
{"$sort", bson.D{
|
||||||
{"_id", 1},
|
{"_id", 1},
|
||||||
}}}
|
}}}
|
||||||
rar, _ := coll.storageAggregate(mongo.Pipeline{matchStage, groupStage, sortStage})
|
rar, _ := coll.storage_aggregate(mongo.Pipeline{matchStage, groupStage, sortStage})
|
||||||
|
|
||||||
ri := convertRarities(rar)
|
ri := convert_rarities(rar)
|
||||||
|
|
||||||
fmt.Printf("%s%s%s\n", Green, sets[0].Name, Reset)
|
LogMessage(fmt.Sprintf("%s", sets[0].Name), "green")
|
||||||
fmt.Printf("Released: %s\n", sets[0].ReleasedAt)
|
LogMessage(fmt.Sprintf("Set Cards: %d/%d", len(cards), sets[0].CardCount), "normal")
|
||||||
fmt.Printf("Set Cards: %d/%d\n", len(cards), sets[0].CardCount)
|
LogMessage(fmt.Sprintf("Total Cards: %.0f", stats[0]["count"]), "normal")
|
||||||
fmt.Printf("Total Cards: %.0f\n", stats[0]["count"])
|
nf_value, err := getFloat64(stats[0]["value"])
|
||||||
fmt.Printf("Foil Cards: %.0f\n", stats[0]["count_foil"])
|
|
||||||
|
|
||||||
normalValue, err := getFloat64(stats[0]["value"])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Error(err)
|
LogMessage(fmt.Sprintf("Error: %v", err), "red")
|
||||||
normalValue = 0
|
nf_value = 0
|
||||||
}
|
}
|
||||||
foilValue, err := getFloat64(stats[0]["value_foil"])
|
foil_value, err := getFloat64(stats[0]["value_foil"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Error(err)
|
LogMessage(fmt.Sprintf("Error: %v", err), "red")
|
||||||
foilValue = 0
|
foil_value = 0
|
||||||
}
|
}
|
||||||
totalValue := normalValue + foilValue
|
total_value := nf_value + foil_value
|
||||||
|
LogMessage(fmt.Sprintf("Total Value: %.2f %s", total_value, getCurrency()), "normal")
|
||||||
normalCount, _ := getFloat64(stats[0]["count"])
|
LogMessage(fmt.Sprintf("Released: %s", sets[0].ReleasedAt), "normal")
|
||||||
foilCount, _ := getFloat64(stats[0]["count_foil"])
|
LogMessage(fmt.Sprintf("Mythics: %.0f", ri.Mythics), "normal")
|
||||||
|
LogMessage(fmt.Sprintf("Rares: %.0f", ri.Rares), "normal")
|
||||||
fmt.Printf("\n%sCurrent Value%s\n", Purple, Reset)
|
LogMessage(fmt.Sprintf("Uncommons: %.0f", ri.Uncommons), "normal")
|
||||||
fmt.Printf("Total: %.0fx %s%.2f%s%s\n", normalCount+foilCount, Yellow, totalValue, getCurrency(), Reset)
|
LogMessage(fmt.Sprintf("Commons: %.0f", ri.Commons), "normal")
|
||||||
fmt.Printf("Normal: %.0fx %s%.2f%s%s\n", stats[0]["count"], Yellow, normalValue, getCurrency(), Reset)
|
|
||||||
fmt.Printf("Foil: %.0fx %s%.2f%s%s\n", stats[0]["count_foil"], Yellow, foilValue, getCurrency(), Reset)
|
|
||||||
|
|
||||||
fmt.Printf("\n%sRarities%s\n", Purple, Reset)
|
|
||||||
fmt.Printf("Mythics: %.0f\n", ri.Mythics)
|
|
||||||
fmt.Printf("Rares: %.0f\n", ri.Rares)
|
|
||||||
fmt.Printf("Uncommons: %.0f\n", ri.Uncommons)
|
|
||||||
fmt.Printf("Commons: %.0f\n", ri.Commons)
|
|
||||||
|
|
||||||
fmt.Printf("\n%sPrice History:%s\n", Pink, Reset)
|
fmt.Printf("\n%sPrice History:%s\n", Pink, Reset)
|
||||||
showPriceHistory(sets[0].SerraPrices, "* ", true)
|
print_price_history(sets[0].SerraPrices, "* ", true)
|
||||||
|
|
||||||
fmt.Printf("\n%sMost valuable cards%s\n", Pink, Reset)
|
fmt.Printf("\n%sMost valuable cards%s\n", Pink, Reset)
|
||||||
|
|
||||||
@ -190,7 +176,8 @@ func ShowSet(setname string) error {
|
|||||||
|
|
||||||
for i := 0; i < ccards; i++ {
|
for i := 0; i < ccards; i++ {
|
||||||
card := cards[i]
|
card := cards[i]
|
||||||
fmt.Printf("* %s%s%s (%s/%s) %s%.2f%s%s\n", Purple, card.Name, Reset, sets[0].Code, card.CollectorNumber, Yellow, card.getValue(false), getCurrency(), Reset)
|
fmt.Printf("* %dx %s%s%s (%s/%s) %s%.2f %s%s\n", card.SerraCount, Purple, card.Name, Reset, sets[0].Code, card.CollectorNumber, Yellow, card.getValue(false), getCurrency(), Reset)
|
||||||
|
fmt.Printf("* %dx %s%s%s (%s/%s) %s%.2f %s%s\n", card.SerraCountFoil, Purple, card.Name, Reset, sets[0].Code, card.CollectorNumber, Yellow, card.getValue(true), getCurrency(), Reset)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
132
src/serra/stats.go
Normal file
132
src/serra/stats.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package serra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(statsCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
var statsCmd = &cobra.Command{
|
||||||
|
Aliases: []string{"stats"},
|
||||||
|
Use: "stats <prefix> <n>",
|
||||||
|
Short: "Shows statistics of the collection",
|
||||||
|
SilenceErrors: true,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
|
client := storage_connect()
|
||||||
|
coll := &Collection{client.Database("serra").Collection("cards")}
|
||||||
|
totalcoll := &Collection{client.Database("serra").Collection("total")}
|
||||||
|
defer storage_disconnect(client)
|
||||||
|
|
||||||
|
sets, _ := coll.storage_aggregate(mongo.Pipeline{
|
||||||
|
bson.D{
|
||||||
|
{"$match", bson.D{
|
||||||
|
{"coloridentity", bson.D{{"$size", 1}}}}}},
|
||||||
|
bson.D{
|
||||||
|
{"$group", bson.D{
|
||||||
|
{"_id", "$coloridentity"},
|
||||||
|
{"count", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count"}}}}}},
|
||||||
|
}}},
|
||||||
|
bson.D{
|
||||||
|
{"$sort", bson.D{
|
||||||
|
{"count", -1},
|
||||||
|
}}},
|
||||||
|
})
|
||||||
|
fmt.Printf("%sColors%s\n", Green, Reset)
|
||||||
|
for _, set := range sets {
|
||||||
|
x, _ := set["_id"].(primitive.A)
|
||||||
|
s := []interface{}(x)
|
||||||
|
fmt.Printf("%s: %s%.0f%s\n", convert_mana_symbols(s), Purple, set["count"], Reset)
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, _ := coll.storage_aggregate(mongo.Pipeline{
|
||||||
|
bson.D{
|
||||||
|
{"$group", bson.D{
|
||||||
|
{"_id", nil},
|
||||||
|
{"value", bson.D{{"$sum", bson.D{{"$multiply", bson.A{getCurrencyField(false), "$serra_count"}}}}}},
|
||||||
|
{"value_foil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{getCurrencyField(true), "$serra_count_foil"}}}}}},
|
||||||
|
{"count", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count"}}}}}},
|
||||||
|
{"count_foil", bson.D{{"$sum", "$serra_count_foil"}}},
|
||||||
|
{"count_etched", bson.D{{"$sum", "$serra_count_etched"}}},
|
||||||
|
{"rarity", bson.D{{"$sum", "$rarity"}}},
|
||||||
|
{"unique", bson.D{{"$sum", 1}}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
bson.D{
|
||||||
|
{"$addFields", bson.D{
|
||||||
|
{"count_all", bson.D{{"$sum", bson.A{"$count", "$count_foil", "$count_etched"}}}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Printf("\n%sCards %s\n", Green, Reset)
|
||||||
|
fmt.Printf("Total: %s%.0f%s\n", Yellow, stats[0]["count_all"], Reset)
|
||||||
|
fmt.Printf("Unique: %s%d%s\n", Purple, stats[0]["unique"], Reset)
|
||||||
|
fmt.Printf("Normal: %s%.0f%s\n", Purple, stats[0]["count"], Reset)
|
||||||
|
fmt.Printf("Foil: %s%d%s\n", Purple, stats[0]["count_foil"], Reset)
|
||||||
|
fmt.Printf("Etched: %s%d%s\n", Purple, stats[0]["count_etched"], Reset)
|
||||||
|
|
||||||
|
reserved, _ := coll.storage_aggregate(mongo.Pipeline{
|
||||||
|
bson.D{
|
||||||
|
{"$match", bson.D{
|
||||||
|
{"reserved", true}}}},
|
||||||
|
bson.D{
|
||||||
|
{"$group", bson.D{
|
||||||
|
{"_id", nil},
|
||||||
|
{"count", bson.D{{"$sum", 1}}},
|
||||||
|
}}},
|
||||||
|
})
|
||||||
|
|
||||||
|
var count_reserved float64
|
||||||
|
if len(reserved) > 0 {
|
||||||
|
count_reserved = reserved[0]["count"].(float64)
|
||||||
|
}
|
||||||
|
fmt.Printf("Reserved List: %s%.0f%s\n", Yellow, count_reserved, Reset)
|
||||||
|
|
||||||
|
rar, _ := coll.storage_aggregate(mongo.Pipeline{
|
||||||
|
bson.D{
|
||||||
|
{"$group", bson.D{
|
||||||
|
{"_id", "$rarity"},
|
||||||
|
{"count", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count"}}}}}},
|
||||||
|
}}},
|
||||||
|
bson.D{
|
||||||
|
{"$sort", bson.D{
|
||||||
|
{"_id", 1},
|
||||||
|
}}},
|
||||||
|
})
|
||||||
|
ri := convert_rarities(rar)
|
||||||
|
fmt.Printf("\n%sRarity%s\n", Green, Reset)
|
||||||
|
fmt.Printf("Mythics: %s%.0f%s\n", Pink, ri.Mythics, Reset)
|
||||||
|
fmt.Printf("Rares: %s%.0f%s\n", Pink, ri.Rares, Reset)
|
||||||
|
fmt.Printf("Uncommons: %s%.0f%s\n", Yellow, ri.Uncommons, Reset)
|
||||||
|
fmt.Printf("Commons: %s%.0f%s\n", Purple, ri.Commons, Reset)
|
||||||
|
|
||||||
|
fmt.Printf("\n%sTotal Value%s\n", Green, Reset)
|
||||||
|
nf_value, err := getFloat64(stats[0]["value"])
|
||||||
|
if err != nil {
|
||||||
|
LogMessage(fmt.Sprintf("Error: %v", err), "red")
|
||||||
|
nf_value = 0
|
||||||
|
}
|
||||||
|
foil_value, err := getFloat64(stats[0]["value_foil"])
|
||||||
|
if err != nil {
|
||||||
|
LogMessage(fmt.Sprintf("Error: %v", err), "red")
|
||||||
|
foil_value = 0
|
||||||
|
}
|
||||||
|
total_value := nf_value + foil_value
|
||||||
|
fmt.Printf("Total: %s%.2f %s%s\n", Pink, total_value, getCurrency(), Reset)
|
||||||
|
fmt.Printf("Normal: %s%.2f %s%s\n", Pink, nf_value, getCurrency(), Reset)
|
||||||
|
fmt.Printf("Foils: %s%.2f %s%s\n", Pink, foil_value, getCurrency(), Reset)
|
||||||
|
total, _ := totalcoll.storage_find_total()
|
||||||
|
|
||||||
|
fmt.Printf("History: \n")
|
||||||
|
print_price_history(total.Value, "* ", true)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ package serra
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -17,8 +18,7 @@ type Total struct {
|
|||||||
Value []PriceEntry `bson:"value"`
|
Value []PriceEntry `bson:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collection Struct
|
// https://siongui.github.io/2017/02/11/go-add-method-function-to-type-in-external-package/
|
||||||
// reason: https://siongui.github.io/2017/02/11/go-add-method-function-to-type-in-external-package/
|
|
||||||
type Collection struct {
|
type Collection struct {
|
||||||
*mongo.Collection
|
*mongo.Collection
|
||||||
}
|
}
|
||||||
@ -45,20 +45,20 @@ func getCurrencyField(foil bool) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func storageConnect() *mongo.Client {
|
func storage_connect() *mongo.Client {
|
||||||
l := Logger()
|
|
||||||
uri := getMongoDBURI()
|
|
||||||
|
|
||||||
|
uri := getMongoDBURI()
|
||||||
client, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(uri))
|
client, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(uri))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalf("Could not connect to mongodb at %s", uri)
|
LogMessage(fmt.Sprintf("Could not connect to mongodb at %s", uri), "red")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return client
|
return client
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (coll Collection) storageAdd(card *Card) error {
|
func (coll Collection) storage_add(card *Card) error {
|
||||||
|
|
||||||
card.SerraUpdated = primitive.NewDateTimeFromTime(time.Now())
|
card.SerraUpdated = primitive.NewDateTimeFromTime(time.Now())
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ func (coll Collection) storageAdd(card *Card) error {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (coll Collection) storageAddSet(set *Set) (*mongo.InsertOneResult, error) {
|
func (coll Collection) storage_add_set(set *Set) (*mongo.InsertOneResult, error) {
|
||||||
|
|
||||||
id, err := coll.InsertOne(context.TODO(), set)
|
id, err := coll.InsertOne(context.TODO(), set)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -80,7 +80,7 @@ func (coll Collection) storageAddSet(set *Set) (*mongo.InsertOneResult, error) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (coll Collection) storageAddTotal(p PriceEntry) error {
|
func (coll Collection) storage_add_total(p PriceEntry) error {
|
||||||
|
|
||||||
// create total object if not exists...
|
// create total object if not exists...
|
||||||
coll.InsertOne(context.TODO(), Total{ID: "1", Value: []PriceEntry{}})
|
coll.InsertOne(context.TODO(), Total{ID: "1", Value: []PriceEntry{}})
|
||||||
@ -101,89 +101,91 @@ func (coll Collection) storageAddTotal(p PriceEntry) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (coll Collection) storageFind(filter, sort bson.D, skip, limit int64) ([]Card, error) {
|
func (coll Collection) storage_find(filter, sort bson.D) ([]Card, error) {
|
||||||
opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit)
|
|
||||||
cursor, err := coll.Find(context.TODO(), filter, opts)
|
|
||||||
l := Logger()
|
|
||||||
|
|
||||||
|
opts := options.Find().SetSort(sort)
|
||||||
|
cursor, err := coll.Find(context.TODO(), filter, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalf("Could not query data due to connection errors to database: %s", err.Error())
|
LogMessage("Could not query data due to connection errors to database", "red")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []Card
|
var results []Card
|
||||||
if err = cursor.All(context.TODO(), &results); err != nil {
|
if err = cursor.All(context.TODO(), &results); err != nil {
|
||||||
l.Fatal(err)
|
log.Fatal(err)
|
||||||
return []Card{}, err
|
return []Card{}, err
|
||||||
}
|
}
|
||||||
return results, nil
|
return results, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (coll Collection) storageFindSet(filter, sort bson.D) ([]Set, error) {
|
func (coll Collection) storage_find_set(filter, sort bson.D) ([]Set, error) {
|
||||||
l := Logger()
|
|
||||||
opts := options.Find().SetSort(sort)
|
|
||||||
|
|
||||||
|
opts := options.Find().SetSort(sort)
|
||||||
cursor, err := coll.Find(context.TODO(), filter, opts)
|
cursor, err := coll.Find(context.TODO(), filter, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalf("Could not query set data due to connection errors to database: %s", err.Error())
|
LogMessage("Could not query set data due to connection errors to database", "red")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []Set
|
var results []Set
|
||||||
if err = cursor.All(context.TODO(), &results); err != nil {
|
if err = cursor.All(context.TODO(), &results); err != nil {
|
||||||
l.Fatal(err)
|
log.Fatal(err)
|
||||||
return []Set{}, err
|
return []Set{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return results, nil
|
return results, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (coll Collection) storageFindTotal() (Total, error) {
|
func (coll Collection) storage_find_total() (Total, error) {
|
||||||
|
|
||||||
var total Total
|
var total Total
|
||||||
l := Logger()
|
|
||||||
|
|
||||||
err := coll.FindOne(context.TODO(), bson.D{{"_id", "1"}}).Decode(&total)
|
err := coll.FindOne(context.TODO(), bson.D{{"_id", "1"}}).Decode(&total)
|
||||||
if err != nil {
|
|
||||||
l.Fatalf("Could not query total data due to connection errors to database: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
LogMessage("Could not query total data due to connection errors to database", "red")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
return total, nil
|
return total, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (coll Collection) storageRemove(filter bson.M) error {
|
func (coll Collection) storage_remove(filter bson.M) error {
|
||||||
l := Logger()
|
|
||||||
|
|
||||||
_, err := coll.DeleteOne(context.TODO(), filter)
|
_, err := coll.DeleteOne(context.TODO(), filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalf("Could remove card data due to connection errors to database: %s", err.Error())
|
LogMessage("Could remove card data due to connection errors to database", "red")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (coll Collection) storageAggregate(pipeline mongo.Pipeline) ([]primitive.M, error) {
|
func (coll Collection) storage_aggregate(pipeline mongo.Pipeline) ([]primitive.M, error) {
|
||||||
l := Logger()
|
|
||||||
opts := options.Aggregate()
|
|
||||||
|
|
||||||
|
opts := options.Aggregate()
|
||||||
cursor, err := coll.Aggregate(
|
cursor, err := coll.Aggregate(
|
||||||
context.TODO(),
|
context.TODO(),
|
||||||
pipeline,
|
pipeline,
|
||||||
opts)
|
opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalf("Could not aggregate data due to connection errors to database: %s", err.Error())
|
LogMessage(fmt.Sprintf("%v", err), "red")
|
||||||
|
LogMessage("Could not aggregate data due to connection errors to database", "red")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a list of all returned documents and print them out.
|
// Get a list of all returned documents and print them out.
|
||||||
// See the mongo.Cursor documentation for more examples of using cursors.
|
// See the mongo.Cursor documentation for more examples of using cursors.
|
||||||
var results []bson.M
|
var results []bson.M
|
||||||
if err = cursor.All(context.TODO(), &results); err != nil {
|
if err = cursor.All(context.TODO(), &results); err != nil {
|
||||||
l.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return results, nil
|
return results, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (coll Collection) storageUpdate(filter, update bson.M) error {
|
func (coll Collection) storage_update(filter, update bson.M) error {
|
||||||
l := Logger()
|
|
||||||
// Call the driver's UpdateOne() method and pass filter and update to it
|
// Call the driver's UpdateOne() method and pass filter and update to it
|
||||||
_, err := coll.UpdateOne(
|
_, err := coll.UpdateOne(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
@ -191,13 +193,14 @@ func (coll Collection) storageUpdate(filter, update bson.M) error {
|
|||||||
update,
|
update,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalf("Could not update data due to connection errors to database: %s", err.Error())
|
LogMessage("Could not update data due to connection errors to database", "red")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func storageDisconnect(client *mongo.Client) error {
|
func storage_disconnect(client *mongo.Client) error {
|
||||||
if err := client.Disconnect(context.TODO()); err != nil {
|
if err := client.Disconnect(context.TODO()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -19,25 +19,24 @@ func init() {
|
|||||||
var updateCmd = &cobra.Command{
|
var updateCmd = &cobra.Command{
|
||||||
Aliases: []string{"u"},
|
Aliases: []string{"u"},
|
||||||
Use: "update",
|
Use: "update",
|
||||||
Short: "update card values from scryfall",
|
Short: "Update card values from scryfall",
|
||||||
Long: `the update mechanism iterates over each card in your collection and fetches its price. after all cards you own in a set are updated, the set value will update. after all sets are updated, the whole collection value is updated.`,
|
Long: `The update mechanism iterates over each card in your collection and fetches its price. After all cards you own in a set are updated, the set value will update. After all Sets are updated, the whole collection value is updated.`,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
client := storageConnect()
|
client := storage_connect()
|
||||||
l := Logger()
|
defer storage_disconnect(client)
|
||||||
defer storageDisconnect(client)
|
|
||||||
|
|
||||||
// update sets
|
// update sets
|
||||||
setscoll := &Collection{client.Database("serra").Collection("sets")}
|
setscoll := &Collection{client.Database("serra").Collection("sets")}
|
||||||
coll := &Collection{client.Database("serra").Collection("cards")}
|
coll := &Collection{client.Database("serra").Collection("cards")}
|
||||||
totalcoll := &Collection{client.Database("serra").Collection("total")}
|
totalcoll := &Collection{client.Database("serra").Collection("total")}
|
||||||
|
|
||||||
// predefine query for set analysis. used for total stats later
|
|
||||||
projectStage := bson.D{{"$project",
|
projectStage := bson.D{{"$project",
|
||||||
bson.D{
|
bson.D{
|
||||||
{"serra_count", true},
|
{"serra_count", true},
|
||||||
{"serra_count_foil", true},
|
{"serra_count_foil", true},
|
||||||
|
{"serra_count_etched", true},
|
||||||
{"set", true},
|
{"set", true},
|
||||||
{"last_price", bson.D{{"$arrayElemAt", bson.A{"$serra_prices", -1}}}}}}}
|
{"last_price", bson.D{{"$arrayElemAt", bson.A{"$serra_prices", -1}}}}}}}
|
||||||
groupStage := bson.D{
|
groupStage := bson.D{
|
||||||
@ -46,42 +45,19 @@ var updateCmd = &cobra.Command{
|
|||||||
{"eur", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.eur", "$serra_count"}}}}}},
|
{"eur", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.eur", "$serra_count"}}}}}},
|
||||||
{"eurfoil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.eur_foil", "$serra_count_foil"}}}}}},
|
{"eurfoil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.eur_foil", "$serra_count_foil"}}}}}},
|
||||||
{"usd", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.usd", "$serra_count"}}}}}},
|
{"usd", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.usd", "$serra_count"}}}}}},
|
||||||
|
{"usdetched", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.usd_etched", "$serra_count_etched"}}}}}},
|
||||||
{"usdfoil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.usd_foil", "$serra_count_foil"}}}}}},
|
{"usdfoil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.usd_foil", "$serra_count_foil"}}}}}},
|
||||||
}}}
|
}}}
|
||||||
|
|
||||||
l.Info("Fetching bulk data from scryfall...")
|
sets, _ := fetch_sets()
|
||||||
downloadURL, err := fetchBulkDownloadURL()
|
|
||||||
if err != nil {
|
|
||||||
l.Error("Could not extract bulk download URL:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
l.Infof("Found latest bulkfile url: %s", downloadURL)
|
|
||||||
|
|
||||||
l.Info("Downloading bulk data file...")
|
|
||||||
bulkFilePath, err := downloadBulkData(downloadURL)
|
|
||||||
if err != nil {
|
|
||||||
l.Error("Could not fetch bulk json from scryfall", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Info("Loading bulk data file...")
|
|
||||||
updatedCards, err := loadBulkFile(bulkFilePath)
|
|
||||||
if err != nil {
|
|
||||||
l.Error("Could not load bulk file:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
l.Infof("Successfully loaded %d cards. Starting Update.", len(updatedCards))
|
|
||||||
|
|
||||||
sets, _ := fetchSets()
|
|
||||||
for _, set := range sets.Data {
|
for _, set := range sets.Data {
|
||||||
|
|
||||||
// When downloading new sets, PriceList needs to be initialized
|
// When downloading new sets, PriceList needs to be initialized
|
||||||
// This query silently fails if set was already downloaded. Not nice but ok for now.
|
// This query silently fails if set was already downloaded. Not nice but ok for now.
|
||||||
// TODO: make this not fail silently
|
|
||||||
set.SerraPrices = []PriceEntry{}
|
set.SerraPrices = []PriceEntry{}
|
||||||
setscoll.storageAddSet(&set)
|
setscoll.storage_add_set(&set)
|
||||||
|
|
||||||
cards, _ := coll.storageFind(bson.D{{"set", set.Code}}, bson.D{{"_id", 1}}, 0, 0)
|
cards, _ := coll.storage_find(bson.D{{"set", set.Code}}, bson.D{{"_id", 1}})
|
||||||
|
|
||||||
// if no cards in collection for this set, skip it
|
// if no cards in collection for this set, skip it
|
||||||
if len(cards) == 0 {
|
if len(cards) == 0 {
|
||||||
@ -98,25 +74,24 @@ var updateCmd = &cobra.Command{
|
|||||||
SaucerHead: "[green]>[reset]",
|
SaucerHead: "[green]>[reset]",
|
||||||
SaucerPadding: " ",
|
SaucerPadding: " ",
|
||||||
BarStart: "|",
|
BarStart: "|",
|
||||||
BarEnd: "| " + set.Name,
|
BarEnd: fmt.Sprintf("| %s%s%s", Pink, set.Name, Reset),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, card := range cards {
|
for _, card := range cards {
|
||||||
bar.Add(1)
|
bar.Add(1)
|
||||||
updatedCard, err := getCardFromBulk(updatedCards, card.Set, card.CollectorNumber)
|
updated_card, err := fetch_card(fmt.Sprintf("%s/%s", card.Set, card.CollectorNumber))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Error(err)
|
LogMessage(fmt.Sprintf("%v", err), "red")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedCard.Prices.Date = primitive.NewDateTimeFromTime(time.Now())
|
updated_card.Prices.Date = primitive.NewDateTimeFromTime(time.Now())
|
||||||
|
|
||||||
update := bson.M{
|
update := bson.M{
|
||||||
"$set": bson.M{"serra_updated": primitive.NewDateTimeFromTime(time.Now()), "prices": updatedCard.Prices, "cmc": updatedCard.Cmc, "cardmarketid": updatedCard.CardmarketID, "tcgplayerid": updatedCard.TCGPlayerID},
|
"$set": bson.M{"serra_updated": primitive.NewDateTimeFromTime(time.Now()), "prices": updated_card.Prices, "collectornumber": updated_card.CollectorNumber},
|
||||||
"$push": bson.M{"serra_prices": updatedCard.Prices},
|
"$push": bson.M{"serra_prices": updated_card.Prices},
|
||||||
}
|
}
|
||||||
coll.storageUpdate(bson.M{"_id": bson.M{"$eq": card.ID}}, update)
|
coll.storage_update(bson.M{"_id": bson.M{"$eq": card.ID}}, update)
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
@ -124,10 +99,10 @@ var updateCmd = &cobra.Command{
|
|||||||
|
|
||||||
// calculate value summary
|
// calculate value summary
|
||||||
matchStage := bson.D{{"$match", bson.D{{"set", set.Code}}}}
|
matchStage := bson.D{{"$match", bson.D{{"set", set.Code}}}}
|
||||||
setValue, _ := coll.storageAggregate(mongo.Pipeline{matchStage, projectStage, groupStage})
|
setvalue, _ := coll.storage_aggregate(mongo.Pipeline{matchStage, projectStage, groupStage})
|
||||||
|
|
||||||
p := PriceEntry{}
|
p := PriceEntry{}
|
||||||
s := setValue[0]
|
s := setvalue[0]
|
||||||
|
|
||||||
p.Date = primitive.NewDateTimeFromTime(time.Now())
|
p.Date = primitive.NewDateTimeFromTime(time.Now())
|
||||||
|
|
||||||
@ -135,26 +110,27 @@ var updateCmd = &cobra.Command{
|
|||||||
mapstructure.Decode(s, &p)
|
mapstructure.Decode(s, &p)
|
||||||
|
|
||||||
// do the update
|
// do the update
|
||||||
setUpdate := bson.M{
|
set_update := bson.M{
|
||||||
"$set": bson.M{"serra_updated": p.Date, "cardcount": set.CardCount},
|
"$set": bson.M{"serra_updated": p.Date},
|
||||||
"$push": bson.M{"serra_prices": p},
|
"$push": bson.M{"serra_prices": p},
|
||||||
}
|
}
|
||||||
setscoll.storageUpdate(bson.M{"code": bson.M{"$eq": set.Code}}, setUpdate)
|
// fmt.Printf("Set %s%s%s (%s) is now worth %s%.02f EUR%s\n", Pink, set.Name, Reset, set.Code, Yellow, setvalue[0]["value"], Reset)
|
||||||
|
setscoll.storage_update(bson.M{"code": bson.M{"$eq": set.Code}}, set_update)
|
||||||
}
|
}
|
||||||
|
|
||||||
totalValue, _ := coll.storageAggregate(mongo.Pipeline{projectStage, groupStage})
|
totalvalue, _ := coll.storage_aggregate(mongo.Pipeline{projectStage, groupStage})
|
||||||
|
|
||||||
t := PriceEntry{}
|
t := PriceEntry{}
|
||||||
t.Date = primitive.NewDateTimeFromTime(time.Now())
|
t.Date = primitive.NewDateTimeFromTime(time.Now())
|
||||||
mapstructure.Decode(totalValue[0], &t)
|
mapstructure.Decode(totalvalue[0], &t)
|
||||||
|
|
||||||
// This is here to be able to fetch currency from
|
// This is here to be able to fetch currency from
|
||||||
// constructed new priceentry
|
// constructed new priceentry
|
||||||
tmpCard := Card{}
|
tmpCard := Card{}
|
||||||
tmpCard.Prices = t
|
tmpCard.Prices = t
|
||||||
|
|
||||||
l.Info("\n%sUpdating total value of collection to: %s%.02f%s%s\n", Green, Yellow, tmpCard.getValue(false)+tmpCard.getValue(true), getCurrency(), Reset)
|
fmt.Printf("\n%sUpdating total value of collection to: %s%.02f %s%s\n", Green, Yellow, tmpCard.getValue(false)+tmpCard.getValue(true), getCurrency(), Reset)
|
||||||
totalcoll.storageAddTotal(t)
|
totalcoll.storage_add_total(t)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
32
src/serra/utils.go
Normal file
32
src/serra/utils.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package serra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
var (
|
||||||
|
Icon = "\U0001F9D9\U0001F3FC"
|
||||||
|
Reset = "\033[0m"
|
||||||
|
Background = "\033[38;5;59m"
|
||||||
|
CurrentLine = "\033[38;5;60m"
|
||||||
|
Foreground = "\033[38;5;231m"
|
||||||
|
Comment = "\033[38;5;103m"
|
||||||
|
Cyan = "\033[38;5;159m"
|
||||||
|
Green = "\033[38;5;120m"
|
||||||
|
Orange = "\033[38;5;222m"
|
||||||
|
Pink = "\033[38;5;212m"
|
||||||
|
Purple = "\033[38;5;183m"
|
||||||
|
Red = "\033[38;5;210m"
|
||||||
|
Yellow = "\033[38;5;229m"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Colored output on commandline
|
||||||
|
func LogMessage(message string, color string) {
|
||||||
|
if color == "red" {
|
||||||
|
fmt.Printf("%s%s%s\n", Red, message, Reset)
|
||||||
|
} else if color == "green" {
|
||||||
|
fmt.Printf("%s%s%s\n", Green, message, Reset)
|
||||||
|
} else if color == "purple" {
|
||||||
|
fmt.Printf("%s%s%s\n", Purple, message, Reset)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s\n", message)
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/serra/web.go
Normal file
58
src/serra/web.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package serra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(webCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
var webCmd = &cobra.Command{
|
||||||
|
Aliases: []string{"a"},
|
||||||
|
Use: "web",
|
||||||
|
Short: "Startup web interface",
|
||||||
|
Long: "Start a tiny web interface to have a web view of your collection",
|
||||||
|
SilenceErrors: true,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
startWeb()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query struct {
|
||||||
|
Name string `form:"name"`
|
||||||
|
Set string `form:"set"`
|
||||||
|
Sort string `form:"sort"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func startWeb() error {
|
||||||
|
|
||||||
|
router := gin.Default()
|
||||||
|
router.LoadHTMLGlob("templates/*.tmpl")
|
||||||
|
router.Static("/assets", "./assets")
|
||||||
|
|
||||||
|
// Landing Page
|
||||||
|
router.GET("/", landingPage)
|
||||||
|
|
||||||
|
router.Run(":8080")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func landingPage(c *gin.Context) {
|
||||||
|
|
||||||
|
var query Query
|
||||||
|
if c.ShouldBind(&query) == nil {
|
||||||
|
cards := Cards("", query.Set, query.Sort, query.Name, "", "")
|
||||||
|
sets := Sets("release")
|
||||||
|
c.HTML(http.StatusOK, "index.tmpl", gin.H{
|
||||||
|
"title": "Serra",
|
||||||
|
"cards": cards,
|
||||||
|
"sets": sets,
|
||||||
|
"version": Version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,10 +4,9 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{{.title}}{{ if .query.Set }} - Set: {{.query.Set}}{{end}}</title>
|
<title>Serra</title>
|
||||||
<link rel="stylesheet" href="https://jenil.github.io/bulmaswatch/cosmo/bulmaswatch.min.css">
|
<!-- <link rel="stylesheet" href="https://unpkg.com/bulma-dracula"> -->
|
||||||
<!-- <link rel="stylesheet" href="https://jenil.github.io/bulmaswatch/lumen/bulmaswatch.min.css"> -->
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
||||||
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css"> -->
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.cardpreview {
|
.cardpreview {
|
||||||
@ -64,10 +63,10 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- Site Title -->
|
<!-- Site Title -->
|
||||||
<section class="hero is-black">
|
<section class="hero is-primary">
|
||||||
<div class="hero-body">
|
<div class="hero-body">
|
||||||
<p class="title">
|
<p class="title">
|
||||||
<a href="/">{{ .title }}</a>
|
{{ .title }}
|
||||||
</p>
|
</p>
|
||||||
<p class="subtitle">
|
<p class="subtitle">
|
||||||
<i>Magic: The Gathering</i> Collection
|
<i>Magic: The Gathering</i> Collection
|
||||||
@ -85,7 +84,7 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Set</label>
|
<label class="label">Set</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<div class="select">
|
<div class="select is-primary">
|
||||||
<select name="set" id="set" form="searchform">
|
<select name="set" id="set" form="searchform">
|
||||||
<option value="">-</option>
|
<option value="">-</option>
|
||||||
{{range .sets}}
|
{{range .sets}}
|
||||||
@ -107,14 +106,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="hidden" id="limit" name="limit" value="500" form="searchform">
|
|
||||||
<input type="hidden" id="page" name="page" value="0" form="searchform">
|
|
||||||
|
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Sort</label>
|
<label class="label">Sort</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<div class="select">
|
<div class="select is-primary">
|
||||||
<select name="sort" id="sort" form="searchform">
|
<select name="sort" id="sort" form="searchform">
|
||||||
<option value="name" selected>Name</option>
|
<option value="name" selected>Name</option>
|
||||||
<option value="value">Value</option>
|
<option value="value">Value</option>
|
||||||
@ -168,7 +164,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{{range .cards}}
|
{{range .cards}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ add .SerraCount .SerraCountFoil }}</td>
|
<td>{{.SerraCount}}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="cardpreview"><strong>{{.Name }}</strong>
|
<div class="cardpreview"><strong>{{.Name }}</strong>
|
||||||
<span class="cardpreviewtext">
|
<span class="cardpreviewtext">
|
||||||
@ -188,74 +184,19 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{{ if ne .numPages 0 }}
|
|
||||||
<div class="hero-body">
|
|
||||||
<nav class="pagination" role="navigation" aria-label="pagination">
|
|
||||||
|
|
||||||
{{ if ge .prevPage 0 }}
|
|
||||||
<a href="/?set={{.query.Set}}&name={{.query.Name}}&sort={{.query.Sort}}&limit={{.limit}}&page={{.prevPage}}" class="pagination-previous">Previous</a>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ if ( le .nextPage .numPages) }}
|
|
||||||
<a href="/?set={{.query.Set}}&name={{.query.Name}}&sort={{.query.Sort}}&limit={{.limit}}&page={{.nextPage}}" class="pagination-next">Next page</a>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
<ul class="pagination-list">
|
|
||||||
|
|
||||||
{{ if ne .page 0 }}
|
|
||||||
<li>
|
|
||||||
<a class="pagination-link" href="/?set={{.query.Set}}&name={{.query.Name}}&sort={{.query.Sort}}&limit={{.limit}}&page=0" aria-label="Goto page 0">0</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<span class="pagination-ellipsis">…</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 -->
|
<!-- Select set from last search -->
|
||||||
<script>
|
<script>
|
||||||
function getParam(paramName) {
|
function getParam(paramName) {
|
||||||
return decodeURI(
|
return decodeURI(
|
||||||
(RegExp(paramName + '=' + '(.+?)(&|$)').exec(location.search) || [, 500])[1]
|
(RegExp(paramName + '=' + '(.+?)(&|$)').exec(location.search) || [, null])[1]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
var selectedSetVal = getParam("set");
|
var selectedSetVal = getParam("set");
|
||||||
document.getElementById("set").value = selectedSetVal;
|
document.getElementById("set").value = selectedSetVal;
|
||||||
|
|
||||||
var selectedLimitVal = getParam("limit");
|
|
||||||
document.getElementById("limit").value = selectedLimitVal;
|
|
||||||
|
|
||||||
var selectedSortVal = getParam("sort");
|
var selectedSortVal = getParam("sort");
|
||||||
document.getElementById("sort").value = selectedSortVal;
|
document.getElementById("sort").value = selectedSortVal;
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user