Compare commits

...

80 Commits
3.11.3 ... main

Author SHA1 Message Date
cea9a47de4 Fix filtering to use queries rather than ad-hoc code 2025-12-23 05:35:20 -05:00
6a24cd6546 Add confirmation menu to removing cards as well 2025-12-22 20:10:11 -05:00
b58718b537 Adding cards now requires confirmation 2025-12-21 11:02:09 -05:00
c4b4967777 Slightly increase image size 2025-12-20 23:27:50 -05:00
3e4a977bef Add basic in-deck tracking 2025-12-20 23:09:45 -05:00
bfa34d5c06 Add initial image support via kitty terminal protocol 2025-12-20 22:00:11 -05:00
Florian Baumann
f75888a1a9 Adjustments for docker image to comply with update step 2025-12-03 08:53:02 +01:00
Florian Baumann
806137de61 Update to go 1.25 2025-12-03 08:11:28 +01:00
Florian Baumann
34686494dd Quit update on err 2025-12-03 08:06:53 +01:00
Florian Baumann
507eef148a Fetch updates from scryfall bulk file
* Switch update to bulkupdate
* Remove tmpfile
* Better output for update
2025-12-03 08:03:59 +01:00
Florian Baumann
a9d1fbc2cd Rename Id to ID 2025-10-17 11:31:34 +02:00
Florian Baumann
3c593f5fdc Remove unused unique flag from removeInteractive 2025-10-17 11:31:18 +02:00
Florian Baumann
7d23bca7f1 Rename count_reserved to countReserved 2025-10-17 11:29:19 +02:00
Florian Baumann
956ae53b59 Rename exportJson to exportJSON 2025-10-17 11:27:58 +02:00
Florian Baumann
7a1803df3a Rename count_all to countAll in stats.go 2025-10-17 11:27:48 +02:00
Florian Baumann
6e961a708c Add package comment 2025-10-17 11:25:32 +02:00
Florian Baumann
dab28a044a Fix export of foils 2025-04-04 09:15:04 +02:00
Florian Baumann
e2c7e54c16 New missing view 2025-03-23 12:05:37 +01:00
Florian Baumann
d2728b455b Add colored output to values 2024-12-10 14:03:44 +01:00
Florian Baumann
cdc38ce2a0 update deps 2024-10-21 11:24:48 +02:00
Florian Baumann
7570d240ab fix fish prompt 2024-10-14 09:46:31 +02:00
Florian Baumann
7143eb84d2 bulk remove script 2024-10-11 09:57:14 +02:00
Florian Baumann
541a4bbd19 Fix missing foil count in web view 2024-09-14 20:23:49 +02:00
Florian Baumann
4d0e75d806 Add url in check detail output 2024-09-05 11:55:17 +02:00
Florian Baumann
fadb1c6aa4 Fix homebrew 2024-09-05 11:46:56 +02:00
Florian Baumann
1c48c5c1e4 Migrate goreleaser to v2 2024-09-05 11:41:23 +02:00
Florian Baumann
0c575021b3 fix check output 2024-09-05 11:35:05 +02:00
Florian Baumann
1229b9d2c6 Fix docker version injection 2024-03-19 13:42:22 +01:00
Florian Baumann
515d32d491 Fix dockerfile issues 2024-03-19 13:38:39 +01:00
Florian Baumann
a42a58f5b9 fix path 2024-03-19 13:30:04 +01:00
Florian Baumann
3794d1813f add cmd 2024-03-19 11:47:13 +01:00
Florian Baumann
4d1fbf0255 fix path 2024-03-19 11:42:23 +01:00
Florian Baumann
23125bea5e Fix bug of empty result 2024-03-19 11:39:08 +01:00
Florian Baumann
f984c69100 fix log output 2024-03-19 11:10:36 +01:00
Florian Baumann
3691890b95 Restructure stats command 2024-03-19 11:04:48 +01:00
Florian Baumann
1ce3920c03 Restructure according to https://github.com/golang-standards/project-layout/ 2024-03-19 10:28:21 +01:00
Florian Baumann
29d6987275 move to scripts 2024-03-19 10:15:29 +01:00
Florian Baumann
7ad859704d Add export to tcghome and moxfield 2024-03-14 09:46:34 +01:00
Florian Baumann
51e139a964 add mongo:6 2024-03-13 11:02:44 +01:00
Florian Baumann
6a88b536bf Mythic filter was missing 2024-02-26 08:08:30 +01:00
Florian Baumann
1be271715c change export format tcgpowertools 2024-02-22 10:08:16 +01:00
Florian Baumann
7bee2d4540 Replace deprecated ioutil with io 2024-02-19 16:08:19 +01:00
Florian Baumann
542fdfd9a6 fix: lowercase error messages 2024-02-19 08:17:16 +01:00
Florian Baumann
84122683c4 Add value output to all add outputs 2024-02-15 12:21:56 +01:00
Florian Baumann
fd5067e66e Fix set name color 2024-02-12 10:18:29 +01:00
Florian Baumann
ee42318b98 Add artist search in card command 2024-02-12 09:28:20 +01:00
Florian Baumann
f2a2b4e65d add cmc card search feature 2024-01-30 12:12:24 +01:00
Florian Baumann
c674e23d93 Remove debug message 2023-11-29 14:58:51 +01:00
Florian Baumann
eb46a898cc add color identity to search 2023-11-29 14:48:03 +01:00
Florian Baumann
2fe24d4781 add export min-count flag 2023-11-27 08:20:00 +01:00
Florian Baumann
3347adadaf Fix count on interactive add 2023-11-10 12:34:06 +01:00
Florian Baumann
af716ae755 Fix missing linebreak on missing command 2023-11-10 11:47:25 +01:00
Florian Baumann
0109d57bd9 Fix interactive foil 2023-11-10 09:31:49 +01:00
Florian Baumann
c5b8ad7270 Add shortcuts to interactive add mode 2023-11-08 14:44:41 +01:00
Florian Baumann
7e7bcd61e9 resturcutre stats 2023-11-08 13:17:27 +01:00
Florian Baumann
58a574b627 Introduced cards added over time 2023-11-08 10:52:09 +01:00
Florian Baumann
1f5e574ca3 Cards added 2023-11-07 17:21:36 +01:00
Florian Baumann
ed7294cadb Goreleaser update 2023-11-02 08:09:53 +01:00
Florian Baumann
240342d52f fix 2023-11-02 07:59:47 +01:00
Florian Baumann
f1c3de2836 Fix format print on set output 2023-11-02 07:48:26 +01:00
Florian Baumann
64607f3680 Add card validation to check 2023-11-02 07:48:26 +01:00
Florian Baumann
f34e49ed71 Update 2023-10-28 07:18:15 +02:00
Florian Baumann
b342a5df75 Add error handling for remove command 2023-10-19 14:32:29 +02:00
Florian Baumann
dcbd275c18 Set increase/decrease log level to warn 2023-10-19 14:30:35 +02:00
Florian Baumann
4cb840558f Add json export 2023-10-18 15:10:25 +02:00
Florian Baumann
07e9a962d2 Fix export 2023-10-18 14:39:02 +02:00
Florian Baumann
0b72fa0265 Add export function 2023-10-18 14:16:00 +02:00
Florian Baumann
c7052c9aec better info on card modifications 2023-09-26 10:19:56 +02:00
Florian Baumann
5578df8e19 Wording 2023-09-15 08:48:44 +02:00
Florian Baumann
a4268afb7f Adjust add output 2023-09-14 17:11:49 +02:00
Florian Baumann
5fb1cb63a2 Fix outputs of missing 2023-09-14 17:08:14 +02:00
Florian Baumann
135f4a93a5 Remove LogMessage and replace with charmbraclet/log 2023-09-14 16:53:08 +02:00
Florian Baumann
71259001d3 remove debug output 2023-09-14 11:16:35 +02:00
Florian Baumann
55e2f72b27 update go 2023-09-14 09:25:38 +02:00
Florian Baumann
ff5575b579 Fixes foil card result list #23 2023-09-13 16:05:17 +02:00
Florian Baumann
cbfbfeef9e fix taskfile release version sorting 2023-09-08 13:20:29 +02:00
Florian Baumann
ef33194e16 Improve wording for check command 2023-08-22 10:42:16 +02:00
Florian Baumann
f0e90350b9 lower setname 2023-08-22 09:50:46 +02:00
Florian Baumann
3870dc8172 Fix: Handle input errors for add commnand 2023-08-14 08:00:36 +02:00
Florian Baumann
23006311d2 Fix search of web 2023-06-30 10:45:22 +02:00
34 changed files with 1265 additions and 608 deletions

3
.gitignore vendored
View File

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

51
.goreleaser.yaml Normal file
View File

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

View File

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

View File

@ -1 +1 @@
golang 1.20
golang 1.21.3

View File

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

View File

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

View File

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

View File

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

80
go.mod
View File

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

216
go.sum
View File

@ -1,73 +1,75 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY=
github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/dolmen-go/kittyimg v0.0.0-20250610224728-874967bd8ea4 h1:KGRb+vxMx5pGsfDjDSW2Th+b2OEflb0yC3s0daCmiYU=
github.com/dolmen-go/kittyimg v0.0.0-20250610224728-874967bd8ea4/go.mod h1:2vk7ATPVcI7uW4Sh6PrSQvtO+Czmq8509xcg/y8Osd0=
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@ -77,138 +79,102 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE=
github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/schollz/progressbar/v3 v3.16.1 h1:RnF1neWZFzLCoGx8yp1yF7SDl4AzNDI5y4I0aUJRrZQ=
github.com/schollz/progressbar/v3 v3.16.1/go.mod h1:I2ILR76gz5VXqYMIY/LdLecvMHDPVcQm3W/MSKi1TME=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.11.7 h1:LIwYxASDLGUg/8wOhgOOZhX8tQa/9tgZPgzZoVqJvcs=
go.mongodb.org/mongo-driver v1.11.7/go.mod h1:G9TgswdsWjX4tmDA5zfs2+6AEPpYJwqblyjsfuh8oXY=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM=
go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@ -2,7 +2,6 @@ package serra
import (
"fmt"
"os"
"strconv"
"strings"
@ -37,9 +36,9 @@ var addCmd = &cobra.Command{
}
func addCardsInteractive(unique bool, set string) {
l := Logger()
if len(set) == 0 {
LogMessage("Error: --set must be given in interactive mode", "red")
os.Exit(1)
l.Fatal("Option --set <set> must be given in interactive mode")
}
rl, err := readline.New(fmt.Sprintf("%s> ", set))
@ -54,6 +53,12 @@ func addCardsInteractive(unique bool, set string) {
break
}
// default is no foil
foil = false
// default is count 1
count = 1
// construct card input for addCards
card := []string{}
@ -73,7 +78,23 @@ func addCardsInteractive(unique bool, set string) {
}
}
} else {
card = append(card, fmt.Sprintf("%s/%s", set, strings.TrimSpace(line)))
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)
@ -84,46 +105,53 @@ func addCardsInteractive(unique bool, set string) {
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")
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
}
// Check if card is already in collection
co, err := coll.storageFind(bson.D{{"set", setName}, {"collectornumber", collectorNumber}}, bson.D{}, 0, 0)
if err != nil {
LogMessage(fmt.Sprintf("%v", err), "red")
l.Error(err)
continue
}
if len(co) >= 1 {
c := co[0]
outputColor := coloredValue(c.getValue(foil))
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")
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
}
modifyCardCount(coll, &c, count, foil)
var total int64 = 0
if foil {
total = c.SerraCountFoil + count
} else {
total = c.SerraCount + count
if askConfirmation(&c) {
modifyCardCount(coll, &c, count, foil)
}
// 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 := fetchCard(setName, collectorNumber)
outputColor := coloredValue(c.getValue(foil))
if err != nil {
LogMessage(fmt.Sprintf("%v", err), "red")
l.Warn(err)
continue
}
@ -131,19 +159,30 @@ func addCards(cards []string, unique bool, count int64) error {
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 {
LogMessage(fmt.Sprintf("%v", err), "red")
l.Warn(err)
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")
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)

View File

@ -1,25 +1,36 @@
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)
}
@ -33,7 +44,7 @@ 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)
cardList := Cards(rarity, set, sortby, name, oracle, cardType, reserved, foil, 0, 0, omitInDeck)
showCardList(cardList, detail)
} else {
ShowCard(cards)
@ -45,11 +56,12 @@ otherwise you'll get a list of cards as a search result.`,
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] == "" {
LogMessage(fmt.Sprintf("Invalid card %s", v), "red")
l.Warnf("Invalid card %s", v)
continue
}
@ -61,7 +73,7 @@ func ShowCard(cardids []string) {
}
}
func Cards(rarity, set, sortby, name, oracle, cardType string, reserved, foil bool, skip, limit int64) []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)
@ -75,6 +87,8 @@ func Cards(rarity, set, sortby, name, oracle, cardType string, reserved, foil bo
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
@ -103,6 +117,14 @@ func Cards(rarity, set, sortby, name, oracle, cardType string, reserved, foil bo
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"}}})
}
@ -111,6 +133,11 @@ func Cards(rarity, set, sortby, name, oracle, cardType string, reserved, foil bo
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})
}
@ -119,8 +146,11 @@ func Cards(rarity, set, sortby, name, oracle, cardType string, reserved, foil bo
filter = append(filter, bson.E{"serra_count_foil", bson.D{{"$gt", 0}}})
}
if count > 0 {
filter = append(filter, bson.E{"serra_count", bson.D{{"$gte", count}}})
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)
@ -133,42 +163,81 @@ func Cards(rarity, set, sortby, name, oracle, cardType string, reserved, foil bo
})
}
// 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 detail {
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 %s%s%s (%s/%s) %s%.2f%s%s\n", card.SerraCount+card.SerraCountFoil+card.SerraCountEtched, Purple, card.Name, Reset, card.Set, card.CollectorNumber, Yellow, card.getValue(false), getCurrency(), Reset)
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)
}
}
fmt.Printf("\nTotal Value: %s%.2f%s%s\n", Yellow, total, getCurrency(), Reset)
if !drawImg {
fmt.Printf("\nTotal Value: %s%.2f%s%s\n", Yellow, total, getCurrency(), Reset)
}
}
func showCardDetails(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("Rarity: %s\n", card.Rarity)
fmt.Printf("Scryfall: %s\n", strings.Replace(card.ScryfallURI, "?utm_source=api", "", 1))
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%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()
}
fmt.Printf("\n%sValue History%s\n", Green, Reset)
showPriceHistory(card.SerraPrices, "* ", false)
fmt.Println()
return nil
}
func drawImage(card *Card) {
fmt.Printf("%s - %s (%s/%s)\n", card.Name, card.SetName, card.Set, card.CollectorNumber)
data, err := base64.StdEncoding.DecodeString(card.SerraImage64)
if err == nil {
reader := bytes.NewReader(data)
img, err := png.Decode(reader)
img = resize.Resize(305, 425, img, resize.Bicubic)
if err == nil {
kittyimg.Fprintln(os.Stdout, img)
}
}
fmt.Println()
}

View File

@ -30,34 +30,45 @@ 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.Split(card, "/")[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 {
LogMessage(fmt.Sprintf("%v", err), "red")
l.Warn(err)
continue
}
// If Card is in collection, print yes.
if len(co) >= 1 {
c := co[0]
LogMessage(fmt.Sprintf("Yes - %s \"%s\" (%s, %.2f%s) is in your Collection", card, c.Name, c.Rarity, c.getValue(foil), getCurrency()), "green")
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)
LogMessage(fmt.Sprintf("No - %s \"%s\" (%s, %.2f%s) is not in your Collection.", card, c.Name, c.Rarity, c.getValue(foil), getCurrency()), "red")
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
LogMessage(fmt.Sprintf("No \"%s\" is not in your Collection.", card), "red")
fmt.Printf("MISSING \"%s\"\n", card)
}
}
}

67
pkg/serra/deck.go Normal file
View File

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

View File

@ -1,7 +1,6 @@
package serra
import (
"log"
"os"
)
@ -9,9 +8,10 @@ const EUR = "€"
const USD = "$"
func getMongoDBURI() string {
l := Logger()
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")
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
@ -20,13 +20,14 @@ func getMongoDBURI() string {
// 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:
LogMessage("Warning: You did not configure SERRA_CURRENCY. Assuming \"USD\"", "yellow")
l.Warn("You did not configure SERRA_CURRENCY. Assuming \"USD\"")
return "$"
}
}

127
pkg/serra/export.go Normal file
View File

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

View File

@ -4,10 +4,12 @@ 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"
)
@ -16,16 +18,48 @@ type Rarities struct {
Rares, Uncommons, Commons, Mythics float64
}
func modifyCardCount(coll *Collection, c *Card, amount int64, foil bool) error {
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
}
storedCard := storedCards[0]
// update card amount
var update bson.M
@ -44,10 +78,49 @@ func modifyCardCount(coll *Collection, c *Card, amount int64, foil bool) error {
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)
}
}
LogMessage(fmt.Sprintf("Updating Card \"%s\" amount to %d", storedCard.Name, total), "purple")
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
}
@ -106,7 +179,6 @@ func convertManaSymbols(sym []interface{}) string {
var mana string
if len(sym) == 0 {
// mana = mana + "\U0001F6AB" //probibited sign for lands
mana = mana + "None" //probibited sign for lands
}
@ -114,19 +186,14 @@ func convertManaSymbols(sym []interface{}) string {
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
@ -221,6 +288,36 @@ func getFloat64(unknown interface{}) (float64, error) {
case uint:
return float64(i), nil
default:
return math.NaN(), errors.New("Non-numeric type could not be converted to float")
return math.NaN(), errors.New("non-numeric type could not be converted to float")
}
}
func coloredValue(value float64) string {
outputColor := Reset
if value > 1 {
outputColor = Green
}
if value > 5 {
outputColor = Yellow
}
if value > 10 {
outputColor = Red
}
return outputColor
}
func askConfirmation(card *Card) bool {
drawImage(card)
fmt.Println("Is this correct (y/n)?")
var char = 'x'
for char != 'y' && char != 'n' {
fmt.Scanf("%c\n", &char)
}
return char == 'y'
}

View File

@ -23,12 +23,13 @@ cards you dont own (yet) :)`,
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 {
LogMessage(fmt.Sprintf("Error: Set %s not found or no card in your collection.", setName[0]), "red")
l.Errorf("Set %s not found or no card in your collection.", setName[0])
return err
}
@ -37,7 +38,7 @@ cards you dont own (yet) :)`,
sets, _ := setcoll.storageFindSet(bson.D{{"code", setName[0]}}, bson.D{{"_id", 1}})
set := sets[0]
LogMessage(fmt.Sprintf("Missing cards in %s", sets[0].Name), "green")
fmt.Printf("Missing cards in %s\n", sets[0].Name)
// generate set with all setnumbers
var (
@ -75,7 +76,7 @@ cards you dont own (yet) :)`,
})
for _, card := range missingCards {
fmt.Printf("%s%s/%s%s %s%.02f%s%s\t%s (%s)\n", Purple, card.Set, card.CollectorNumber, Reset, Green, card.getValue(false), Reset, getCurrency(), card.Name, card.SetName)
fmt.Printf("%s%s/%s\t%s(%s, %shttps://scryfall.com/card/%s/%s%s)\t%s%.02f%s%s\t%s (%s)\n", Purple, card.Set, card.CollectorNumber, Reset, string([]rune(card.Rarity)[0]), Background, card.Set, card.CollectorNumber, Reset, Green, card.getValue(false), Reset, getCurrency(), card.Name, card.SetName)
}
return nil

View File

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

View File

@ -1,22 +1,29 @@
// Package serra
//
// It implements base functions and also cli wrappers
// The entire tool consists only of this one package.
package serra
import (
"fmt"
"os"
"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
@ -37,8 +44,9 @@ var rootCmd = &cobra.Command{
}
func Execute() {
l := Logger()
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
l.Fatal(err)
}
}

View File

@ -2,11 +2,14 @@ package serra
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
@ -14,18 +17,23 @@ import (
type Card struct {
// Added by Serra
SerraCount int64 `bson:"serra_count"`
SerraCountFoil int64 `bson:"serra_count_foil"`
SerraCountEtched int64 `bson:"serra_count_etched"`
SerraPrices []PriceEntry `bson:"serra_prices"`
SerraCreated primitive.DateTime `bson:"serra_created"`
SerraUpdated primitive.DateTime `bson:"serra_updated"`
SerraCount int64 `bson:"serra_count"`
SerraCountFoil int64 `bson:"serra_count_foil"`
SerraCountEtched int64 `bson:"serra_count_etched"`
SerraCountDeck int64 `bson:"serra_count_deck"`
SerraCountFoilDeck int64 `bson:"serra_count_foil_deck"`
SerraCountEtchedDeck int64 `bson:"serra_count_etched_deck"`
SerraPrices []PriceEntry `bson:"serra_prices"`
SerraCreated primitive.DateTime `bson:"serra_created"`
SerraUpdated primitive.DateTime `bson:"serra_updated"`
SerraImage64 string `bson:"serra_image"`
Artist string `json:"artist"`
ArtistIds []string `json:"artist_ids"`
Booster bool `json:"booster"`
BorderColor string `json:"border_color"`
CardBackID string `json:"card_back_id"`
CardmarketID float64 `json:"cardmarket_id"`
Cmc float64 `json:"cmc"`
CollectorNumber string `json:"collector_number"`
ColorIdentity []string `json:"color_identity"`
@ -98,37 +106,139 @@ type Card struct {
TcgplayerInfiniteArticles string `json:"tcgplayer_infinite_articles"`
TcgplayerInfiniteDecks string `json:"tcgplayer_infinite_decks"`
} `json:"related_uris"`
ReleasedAt string `json:"released_at"`
Reprint bool `json:"reprint"`
Reserved bool `json:"reserved"`
RulingsURI string `json:"rulings_uri"`
ScryfallSetURI string `json:"scryfall_set_uri"`
ScryfallURI string `json:"scryfall_uri"`
Set string `json:"set"`
SetID string `json:"set_id"`
SetName string `json:"set_name"`
SetSearchURI string `json:"set_search_uri"`
SetType string `json:"set_type"`
SetURI string `json:"set_uri"`
StorySpotlight bool `json:"story_spotlight"`
Textless bool `json:"textless"`
TypeLine string `json:"type_line"`
URI string `json:"uri"`
Variation bool `json:"variation"`
ReleasedAt string `json:"released_at"`
Reprint bool `json:"reprint"`
Reserved bool `json:"reserved"`
RulingsURI string `json:"rulings_uri"`
ScryfallSetURI string `json:"scryfall_set_uri"`
ScryfallURI string `json:"scryfall_uri"`
Set string `json:"set"`
SetID string `json:"set_id"`
SetName string `json:"set_name"`
SetSearchURI string `json:"set_search_uri"`
SetType string `json:"set_type"`
SetURI string `json:"set_uri"`
StorySpotlight bool `json:"story_spotlight"`
Textless bool `json:"textless"`
TCGPlayerID float64 `json:"tcgplayer_id"`
TypeLine string `json:"type_line"`
URI string `json:"uri"`
Variation bool `json:"variation"`
}
// Getter for currency specific value
func (c Card) getValue(foil bool) float64 {
if getCurrency() == EUR {
if foil {
return c.Prices.EurFoil
type BulkIndex struct {
Object string `json:"object"`
HasMore bool `json:"has_more"`
Data []struct {
Object string `json:"object"`
ID string `json:"id"`
Type string `json:"type"`
UpdatedAt time.Time `json:"updated_at"`
URI string `json:"uri"`
Name string `json:"name"`
Description string `json:"description"`
Size int `json:"size"`
DownloadURI string `json:"download_uri"`
ContentType string `json:"content_type"`
ContentEncoding string `json:"content_encoding"`
} `json:"data"`
}
func fetchBulkDownloadURL() (string, error) {
url := "https://api.scryfall.com/bulk-data"
downloadURL := ""
// Make an HTTP GET request
resp, err := http.Get(url)
if err != nil {
log.Fatalf("Error fetching data: %v", err)
}
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 c.Prices.UsdFoil
return downloadURL, nil
}
func downloadBulkData(downloadURL string) (string, error) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "download")
if err != nil {
log.Fatalf("Error creating temporary directory: %v", err)
}
return c.Prices.Usd
// defer os.RemoveAll(tempDir) // Clean up the directory when done
// Create a temporary file in the temporary directory
tempFile, err := os.CreateTemp(tempDir, "downloaded-*.json") // Adjust the extension if necessary
if err != nil {
log.Fatalf("Error creating temporary file: %v", err)
}
// defer tempFile.Close() // Ensure we close the file when we're done
// Download the file
resp, err := http.Get(downloadURL)
if err != nil {
log.Fatalf("Error downloading file: %v", err)
}
defer resp.Body.Close() // Make sure to close the response body
// Check for a successful response
if resp.StatusCode != http.StatusOK {
log.Fatalf("Error: received status code %d", resp.StatusCode)
}
// Copy the response body to the temporary file
_, err = io.Copy(tempFile, resp.Body)
if err != nil {
log.Fatalf("Error saving file: %v", err)
}
return tempFile.Name(), nil
}
func loadBulkFile(bulkFilePath string) ([]Card, error) {
var cards []Card
fileBytes, _ := os.ReadFile(bulkFilePath)
defer os.Remove(bulkFilePath)
err := json.Unmarshal(fileBytes, &cards)
if err != nil {
fmt.Println("Error unmarshalling bulk file:", err)
return cards, nil
}
return cards, nil
}
func getCardFromBulk(cards []Card, setName, collectorNumber string) (*Card, error) {
var foundCard Card
for _, v := range cards {
if v.CollectorNumber == collectorNumber && v.Set == setName {
foundCard = v
return &foundCard, nil
}
}
return &Card{}, fmt.Errorf("Card %s/%s not found in bulk data", setName, collectorNumber)
}
type PriceEntry struct {
@ -168,6 +278,20 @@ type Set struct {
URI string `json:"uri"`
}
// Getter for currency specific value
func (c Card) getValue(foil bool) float64 {
if getCurrency() == EUR {
if foil {
return c.Prices.EurFoil
}
return c.Prices.Eur
}
if foil {
return c.Prices.UsdFoil
}
return c.Prices.Usd
}
func fetchCard(setName, collectorNumber string) (*Card, error) {
resp, err := http.Get(fmt.Sprintf("https://api.scryfall.com/cards/%s/%s/", setName, collectorNumber))
if err != nil {
@ -179,8 +303,8 @@ func fetchCard(setName, collectorNumber string) (*Card, error) {
return &Card{}, fmt.Errorf("Card %s/%s not found", setName, collectorNumber)
}
//We Read the response body on the line below.
body, err := ioutil.ReadAll(resp.Body)
//we read the response body on the line below.
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("%s", err)
return &Card{}, err
@ -189,7 +313,6 @@ func fetchCard(setName, collectorNumber string) (*Card, error) {
r := bytes.NewReader(body)
decoder := json.NewDecoder(r)
val := &Card{}
err = decoder.Decode(val)
if err != nil {
log.Fatalf("%s", err)
@ -202,11 +325,24 @@ func fetchCard(setName, collectorNumber string) (*Card, error) {
val.Prices.Date = primitive.NewDateTimeFromTime(time.Now())
val.SerraPrices = append(val.SerraPrices, val.Prices)
imgResp, imgErr := http.Get(val.ImageUris.Png)
if imgErr != nil {
log.Fatalln(err)
return &Card{}, err
}
bytes, readErr := io.ReadAll(imgResp.Body)
if readErr != nil {
log.Fatalln(err)
return &Card{}, err
}
val.SerraImage64 = base64.StdEncoding.EncodeToString(bytes)
return val, nil
}
func fetchSets() (*SetList, error) {
// TODO better URL Building...
// TODO: better URL Building...
resp, err := http.Get("https://api.scryfall.com/sets")
if err != nil {
log.Fatalln(err)
@ -218,7 +354,7 @@ func fetchSets() (*SetList, error) {
}
//We Read the response body on the line below.
body, err := ioutil.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalln(err)
return &SetList{}, err

View File

@ -88,6 +88,7 @@ func ShowSet(setname string) error {
client := storageConnect()
coll := &Collection{client.Database("serra").Collection("cards")}
l := Logger()
defer storageDisconnect(client)
// fetch all cards in set ordered by currently used currency
@ -97,7 +98,7 @@ func ShowSet(setname string) error {
}
cards, err := coll.storageFind(bson.D{{"set", setname}}, cardSortCurrency, 0, 0)
if (err != nil) || len(cards) == 0 {
LogMessage(fmt.Sprintf("Error: Set %s not found or no card in your collection.", setname), "red")
l.Errorf("Set %s not found or no card in your collection.", setname)
return err
}
@ -142,7 +143,7 @@ func ShowSet(setname string) error {
ri := convertRarities(rar)
LogMessage(sets[0].Name, "green")
fmt.Printf("%s%s%s\n", Green, sets[0].Name, Reset)
fmt.Printf("Released: %s\n", sets[0].ReleasedAt)
fmt.Printf("Set Cards: %d/%d\n", len(cards), sets[0].CardCount)
fmt.Printf("Total Cards: %.0f\n", stats[0]["count"])
@ -150,12 +151,12 @@ func ShowSet(setname string) error {
normalValue, err := getFloat64(stats[0]["value"])
if err != nil {
LogMessage(fmt.Sprintf("Error: %v", err), "red")
l.Error(err)
normalValue = 0
}
foilValue, err := getFloat64(stats[0]["value_foil"])
if err != nil {
LogMessage(fmt.Sprintf("Error: %v", err), "red")
l.Error(err)
foilValue = 0
}
totalValue := normalValue + foilValue

245
pkg/serra/stats.go Normal file
View File

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

View File

@ -3,7 +3,6 @@ package serra
import (
"context"
"fmt"
"log"
"os"
"time"
@ -18,7 +17,8 @@ type Total struct {
Value []PriceEntry `bson:"value"`
}
// https://siongui.github.io/2017/02/11/go-add-method-function-to-type-in-external-package/
// Collection Struct
// reason: https://siongui.github.io/2017/02/11/go-add-method-function-to-type-in-external-package/
type Collection struct {
*mongo.Collection
}
@ -46,12 +46,12 @@ func getCurrencyField(foil bool) string {
}
func storageConnect() *mongo.Client {
l := Logger()
uri := getMongoDBURI()
client, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(uri))
if err != nil {
LogMessage(fmt.Sprintf("Could not connect to mongodb at %s", uri), "red")
os.Exit(1)
l.Fatalf("Could not connect to mongodb at %s", uri)
}
return client
@ -104,14 +104,15 @@ func (coll Collection) storageAddTotal(p PriceEntry) error {
func (coll Collection) storageFind(filter, sort bson.D, skip, limit int64) ([]Card, error) {
opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit)
cursor, err := coll.Find(context.TODO(), filter, opts)
l := Logger()
if err != nil {
LogMessage(fmt.Sprintf("Could not query data due to connection errors to database: %s", err.Error()), "red")
os.Exit(1)
l.Fatalf("Could not query data due to connection errors to database: %s", err.Error())
}
var results []Card
if err = cursor.All(context.TODO(), &results); err != nil {
log.Fatal(err)
l.Fatal(err)
return []Card{}, err
}
return results, nil
@ -119,17 +120,17 @@ func (coll Collection) storageFind(filter, sort bson.D, skip, limit int64) ([]Ca
}
func (coll Collection) storageFindSet(filter, sort bson.D) ([]Set, error) {
l := Logger()
opts := options.Find().SetSort(sort)
cursor, err := coll.Find(context.TODO(), filter, opts)
if err != nil {
LogMessage(fmt.Sprintf("Could not query set data due to connection errors to database: %s", err.Error()), "red")
os.Exit(1)
l.Fatalf("Could not query set data due to connection errors to database: %s", err.Error())
}
var results []Set
if err = cursor.All(context.TODO(), &results); err != nil {
log.Fatal(err)
l.Fatal(err)
return []Set{}, err
}
@ -138,27 +139,29 @@ func (coll Collection) storageFindSet(filter, sort bson.D) ([]Set, error) {
func (coll Collection) storageFindTotal() (Total, error) {
var total Total
l := Logger()
err := coll.FindOne(context.TODO(), bson.D{{"_id", "1"}}).Decode(&total)
if err != nil {
LogMessage(fmt.Sprintf("Could not query total data due to connection errors to database: %s", err.Error()), "red")
os.Exit(1)
l.Fatalf("Could not query total data due to connection errors to database: %s", err.Error())
}
return total, nil
}
func (coll Collection) storageRemove(filter bson.M) error {
l := Logger()
_, err := coll.DeleteOne(context.TODO(), filter)
if err != nil {
LogMessage(fmt.Sprintf("Could remove card data due to connection errors to database: %s", err.Error()), "red")
os.Exit(1)
l.Fatalf("Could remove card data due to connection errors to database: %s", err.Error())
}
return nil
}
func (coll Collection) storageAggregate(pipeline mongo.Pipeline) ([]primitive.M, error) {
l := Logger()
opts := options.Aggregate()
cursor, err := coll.Aggregate(
@ -166,21 +169,21 @@ func (coll Collection) storageAggregate(pipeline mongo.Pipeline) ([]primitive.M,
pipeline,
opts)
if err != nil {
LogMessage(fmt.Sprintf("Could not aggregate data due to connection errors to database: %s", err.Error()), "red")
os.Exit(1)
l.Fatalf("Could not aggregate data due to connection errors to database: %s", err.Error())
}
// Get a list of all returned documents and print them out.
// See the mongo.Cursor documentation for more examples of using cursors.
var results []bson.M
if err = cursor.All(context.TODO(), &results); err != nil {
log.Fatal(err)
l.Fatal(err)
}
return results, nil
}
func (coll Collection) storageUpdate(filter, update bson.M) error {
l := Logger()
// Call the driver's UpdateOne() method and pass filter and update to it
_, err := coll.UpdateOne(
context.Background(),
@ -188,8 +191,7 @@ func (coll Collection) storageUpdate(filter, update bson.M) error {
update,
)
if err != nil {
LogMessage(fmt.Sprintf("Could not update data due to connection errors to database: %s", err.Error()), "red")
os.Exit(1)
l.Fatalf("Could not update data due to connection errors to database: %s", err.Error())
}
return nil

23
pkg/serra/undeck.go Normal file
View File

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

View File

@ -19,12 +19,13 @@ func init() {
var updateCmd = &cobra.Command{
Aliases: []string{"u"},
Use: "update",
Short: "Update card values from scryfall",
Long: `The update mechanism iterates over each card in your collection and fetches its price. After all cards you own in a set are updated, the set value will update. After all Sets are updated, the whole collection value is updated.`,
Short: "update card values from scryfall",
Long: `the update mechanism iterates over each card in your collection and fetches its price. after all cards you own in a set are updated, the set value will update. after all sets are updated, the whole collection value is updated.`,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
client := storageConnect()
l := Logger()
defer storageDisconnect(client)
// update sets
@ -48,11 +49,35 @@ var updateCmd = &cobra.Command{
{"usdfoil", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$last_price.usd_foil", "$serra_count_foil"}}}}}},
}}}
l.Info("Fetching bulk data from scryfall...")
downloadURL, err := fetchBulkDownloadURL()
if err != nil {
l.Error("Could not extract bulk download URL:", err)
return err
}
l.Infof("Found latest bulkfile url: %s", downloadURL)
l.Info("Downloading bulk data file...")
bulkFilePath, err := downloadBulkData(downloadURL)
if err != nil {
l.Error("Could not fetch bulk json from scryfall", err)
return err
}
l.Info("Loading bulk data file...")
updatedCards, err := loadBulkFile(bulkFilePath)
if err != nil {
l.Error("Could not load bulk file:", err)
return err
}
l.Infof("Successfully loaded %d cards. Starting Update.", len(updatedCards))
sets, _ := fetchSets()
for _, set := range sets.Data {
// When downloading new sets, PriceList needs to be initialized
// This query silently fails if set was already downloaded. Not nice but ok for now.
// TODO: make this not fail silently
set.SerraPrices = []PriceEntry{}
setscoll.storageAddSet(&set)
@ -73,22 +98,22 @@ var updateCmd = &cobra.Command{
SaucerHead: "[green]>[reset]",
SaucerPadding: " ",
BarStart: "|",
BarEnd: fmt.Sprintf("| %s%s%s", Pink, set.Name, Reset),
BarEnd: "| " + set.Name,
}),
)
for _, card := range cards {
bar.Add(1)
updatedCard, err := fetchCard(card.Set, card.CollectorNumber)
updatedCard, err := getCardFromBulk(updatedCards, card.Set, card.CollectorNumber)
if err != nil {
LogMessage(fmt.Sprintf("%v", err), "red")
l.Error(err)
continue
}
updatedCard.Prices.Date = primitive.NewDateTimeFromTime(time.Now())
update := bson.M{
"$set": bson.M{"serra_updated": primitive.NewDateTimeFromTime(time.Now()), "prices": updatedCard.Prices, "cmc": updatedCard.Cmc},
"$set": bson.M{"serra_updated": primitive.NewDateTimeFromTime(time.Now()), "prices": updatedCard.Prices, "cmc": updatedCard.Cmc, "cardmarketid": updatedCard.CardmarketID, "tcgplayerid": updatedCard.TCGPlayerID},
"$push": bson.M{"serra_prices": updatedCard.Prices},
}
coll.storageUpdate(bson.M{"_id": bson.M{"$eq": card.ID}}, update)
@ -114,7 +139,6 @@ var updateCmd = &cobra.Command{
"$set": bson.M{"serra_updated": p.Date, "cardcount": set.CardCount},
"$push": bson.M{"serra_prices": p},
}
// fmt.Printf("Set %s%s%s (%s) is now worth %s%.02f EUR%s\n", Pink, set.Name, Reset, set.Code, Yellow, setvalue[0]["value"], Reset)
setscoll.storageUpdate(bson.M{"code": bson.M{"$eq": set.Code}}, setUpdate)
}
@ -129,7 +153,7 @@ var updateCmd = &cobra.Command{
tmpCard := Card{}
tmpCard.Prices = t
fmt.Printf("\n%sUpdating total value of collection to: %s%.02f%s%s\n", Green, Yellow, tmpCard.getValue(false)+tmpCard.getValue(true), getCurrency(), Reset)
l.Info("\n%sUpdating total value of collection to: %s%.02f%s%s\n", Green, Yellow, tmpCard.getValue(false)+tmpCard.getValue(true), getCurrency(), Reset)
totalcoll.storageAddTotal(t)
return nil

View File

@ -3,6 +3,7 @@ package serra
import (
"net/http"
"strconv"
"text/template"
"github.com/gin-gonic/gin"
"github.com/spf13/cobra"
@ -16,6 +17,10 @@ func init() {
rootCmd.AddCommand(webCmd)
}
func add(a, b int64) int64 {
return a + b
}
var webCmd = &cobra.Command{
Aliases: []string{"a"},
Use: "web",
@ -38,6 +43,9 @@ type Query struct {
func startWeb() error {
router := gin.Default()
router.SetFuncMap(template.FuncMap{
"add": add,
})
router.LoadHTMLGlob("templates/*.tmpl")
router.Static("/assets", "./assets")
@ -63,7 +71,7 @@ func landingPage(c *gin.Context) {
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)
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{}
@ -89,7 +97,12 @@ func landingPage(c *gin.Context) {
}}},
})
defer storageDisconnect(client)
numCards := counts[0]["count"].(int32)
// 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",

19
scripts/removebulk.fish Executable file
View File

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

View File

@ -1,170 +0,0 @@
package serra
import (
"fmt"
"github.com/spf13/cobra"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
func init() {
rootCmd.AddCommand(statsCmd)
}
var statsCmd = &cobra.Command{
Aliases: []string{"stats"},
Use: "stats <prefix> <n>",
Short: "Shows statistics of the collection",
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
client := storageConnect()
coll := &Collection{client.Database("serra").Collection("cards")}
totalcoll := &Collection{client.Database("serra").Collection("total")}
defer storageDisconnect(client)
// Colors
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("%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)
}
// 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("\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)
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 count_reserved int32
if len(reserved) > 0 {
count_reserved = reserved[0]["count"].(int32)
}
fmt.Printf("Reserved List: %s%d%s\n", Yellow, count_reserved, Reset)
// Rarities
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)
// Artists
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)
}
// Mana Curve of Collection
cmc, _ := coll.storageAggregate(mongo.Pipeline{
bson.D{
{"$group", bson.D{
{"_id", "$cmc"},
// {"count", bson.D{{"$sum", bson.D{{"$multiply", bson.A{1.0, "$serra_count"}}}}}},
{"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)
}
// Total Value
fmt.Printf("\n%sTotal Value%s\n", Green, Reset)
normalValue, err := getFloat64(stats[0]["value"])
if err != nil {
LogMessage(fmt.Sprintf("Error: %v", err), "red")
normalValue = 0
}
foilValue, err := getFloat64(stats[0]["value_foil"])
if err != nil {
LogMessage(fmt.Sprintf("Error: %v", err), "red")
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)
total, _ := totalcoll.storageFindTotal()
fmt.Printf("History: \n")
showPriceHistory(total.Value, "* ", true)
return nil
},
}

View File

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

View File

@ -168,7 +168,7 @@
<tbody>
{{range .cards}}
<tr>
<td>{{.SerraCount}}</td>
<td>{{ add .SerraCount .SerraCountFoil }}</td>
<td>
<div class="cardpreview"><strong>{{.Name }}</strong>
<span class="cardpreviewtext">
@ -247,7 +247,7 @@
<script>
function getParam(paramName) {
return decodeURI(
(RegExp(paramName + '=' + '(.+?)(&|$)').exec(location.search) || [, null])[1]
(RegExp(paramName + '=' + '(.+?)(&|$)').exec(location.search) || [, 500])[1]
);
}
var selectedSetVal = getParam("set");