Compare commits

...

47 Commits
3.14.0 ... 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
33 changed files with 1070 additions and 621 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
repository:
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,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,10 +5,10 @@ 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"

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

86
go.mod
View File

@ -1,33 +1,67 @@
module github.com/noqqe/serra
go 1.14
go 1.25.0
toolchain go1.25.4
require (
github.com/bytedance/sonic v1.10.2 // indirect
github.com/charmbracelet/lipgloss v0.9.1 // indirect
github.com/charmbracelet/log v0.2.5
github.com/charmbracelet/log v0.4.0
github.com/chzyer/readline v1.5.1
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-gonic/gin v1.9.1
github.com/go-playground/validator/v10 v10.15.5 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/klauspost/compress v1.17.2 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // 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/pelletier/go-toml/v2 v2.1.0 // 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/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
go.mongodb.org/mongo-driver v1.12.1
golang.org/x/arch v0.5.0 // indirect
golang.org/x/sync v0.4.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/protobuf v1.31.0 // 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
)

219
go.sum
View File

@ -1,39 +1,40 @@
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.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/charmbracelet/lipgloss v0.8.0/go.mod h1:p4eYUZZJ/0oXTuCQKFF8mqyKCz0ja6y+7DniDDw5KKU=
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
github.com/charmbracelet/log v0.2.5 h1:1yVvyKCKVV639RR4LIq1iy1Cs1AKxuNO+Hx2LJtk7Wc=
github.com/charmbracelet/log v0.2.5/go.mod h1:nQGK8tvc4pS9cvVEH/pWJiZ50eUq1aoXUOjGpXvdD0k=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
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/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
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=
@ -42,54 +43,33 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
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.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24=
github.com/go-playground/validator/v10 v10.15.5/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.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
github.com/klauspost/compress v1.17.2/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/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/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/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.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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=
@ -99,31 +79,24 @@ 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/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
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=
@ -134,108 +107,74 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
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/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
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.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE=
go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
golang.org/x/arch v0.5.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.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
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/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
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-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.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
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.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.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.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.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/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.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=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -134,17 +134,22 @@ func addCards(cards []string, unique bool, count int64) error {
if len(co) >= 1 {
c := co[0]
outputColor := coloredValue(c.getValue(foil))
if unique {
l.Warnf("%dx \"%s\" (%s, %.2f%s) not added, because it already exists", count, c.Name, c.Rarity, c.getValue(foil), getCurrency())
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)
if askConfirmation(&c) {
modifyCardCount(coll, &c, count, foil)
}
} else {
// Fetch card from scryfall
c, err := fetchCard(setName, collectorNumber)
outputColor := coloredValue(c.getValue(foil))
if err != nil {
l.Warn(err)
continue
@ -154,11 +159,18 @@ 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 {
l.Warn(err)
@ -167,9 +179,9 @@ func addCards(cards []string, unique bool, count int64) error {
// Give feedback of successfully added card
if foil {
l.Infof("%dx \"%s\" (%s, %.2f%s, foil) added", total, c.Name, c.Rarity, c.getValue(foil), getCurrency())
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, %.2f%s) added", total, c.Name, c.Rarity, c.getValue(foil), getCurrency())
l.Infof("%dx \"%s\" (%s, %s%.2f%s%s) added", total, c.Name, c.Rarity, outputColor, c.getValue(foil), getCurrency(), Reset)
}
}
}

View File

@ -1,19 +1,27 @@
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")
@ -21,6 +29,8 @@ func init() {
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)
}
@ -34,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)
@ -63,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)
@ -77,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
@ -105,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"}}})
}
@ -126,6 +146,13 @@ 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 omitInDeck {
filter = append(filter, bson.E{ "$expr", bson.D{{"$or",bson.A{
bson.D{{"$ne", bson.A{"$serra_count", "$serra_count_deck"}}},
bson.D{{"$ne", bson.A{"$serra_count_foil", "$serra_count_foil_deck"}}},
}}}})
}
cards, _ := coll.storageFind(filter, sortStage, skip, limit)
// This is needed because collectornumbers are strings (ie. "23a") but still we
@ -153,36 +180,64 @@ func Cards(rarity, set, sortby, name, oracle, cardType string, reserved, foil bo
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

@ -59,16 +59,16 @@ func checkCards(cards []string, detail bool) error {
// If Card is in collection, print yes.
if len(co) >= 1 {
c := co[0]
fmt.Printf("PRESENT %s \"%s\" (%s, %.2f%s)", card, c.Name, c.Rarity, c.getValue(foil), getCurrency())
fmt.Printf("PRESENT %s \"%s\" (%s, %.2f%s) %s\n", card, c.Name, c.Rarity, c.getValue(foil), getCurrency(), strings.Replace(c.ScryfallURI, "?utm_source=api", "", 1))
continue
} else {
if detail {
// fetch card from scyrfall if --detail was given
c, _ := fetchCard(setName, collectorNumber)
fmt.Printf("MISSING %s \"%s\" (%s, %.2f%s)", card, c.Name, c.Rarity, c.getValue(foil), getCurrency())
fmt.Printf("MISSING %s \"%s\" (%s, %.2f%s) %s\n", card, c.Name, c.Rarity, c.getValue(foil), getCurrency(), strings.Replace(c.ScryfallURI, "?utm_source=api", "", 1))
} else {
// Just print, the card name was not found
fmt.Printf("MISSING \"%s\"", card)
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
}

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

@ -41,17 +41,25 @@ func Logger() *log.Logger {
return l
}
func modifyCardCount(coll *Collection, c *Card, amount int64, foil bool) error {
func getStoredCard(coll *Collection, c *Card) (Card, error) {
// find already existing card
sort := bson.D{{"_id", 1}}
searchFilter := bson.D{{"_id", c.ID}}
l := Logger()
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
@ -71,22 +79,51 @@ func modifyCardCount(coll *Collection, c *Card, amount int64, foil bool) error {
if foil {
total = storedCard.SerraCountFoil + amount
if amount < 0 {
l.Warnf("Reduced card amount of \"%s\" (foil) from %d to %d", storedCard.Name, storedCard.SerraCountFoil, total)
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\" (foil) from %d to %d", storedCard.Name, storedCard.SerraCountFoil, total)
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\" from %d to %d", storedCard.Name, storedCard.SerraCount, total)
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\" from %d to %d", storedCard.Name, storedCard.SerraCount, total)
l.Warnf("Increased card amount of \"%s\" (%.2f%s) from %d to %d", storedCard.Name, storedCard.getValue(false), getCurrency(), storedCard.SerraCount, total)
}
}
return nil
}
func modifyCardDeckCount(coll *Collection, c *Card, amount int64, foil bool) error {
l := Logger()
storedCard, err := getStoredCard(coll, c)
if err != nil {
return err
}
// update card amount
var update bson.M
if foil {
newAmount := min(max(storedCard.SerraCountFoilDeck + amount, 0), storedCard.SerraCountFoil)
l.Infof("%d / %d available", storedCard.SerraCountFoil - newAmount, storedCard.SerraCountFoil)
update = bson.M{
"$set": bson.M{"serra_count_foil_deck": newAmount},
}
} else {
newAmount := min(max(storedCard.SerraCountDeck + amount, 0), storedCard.SerraCount)
l.Infof("%d / %d available", storedCard.SerraCount - newAmount, storedCard.SerraCount)
update = bson.M{
"$set": bson.M{"serra_count_deck": newAmount},
}
}
coll.storageUpdate(bson.M{"_id": bson.M{"$eq": c.ID}}, update)
return nil
}
func findCardByCollectorNumber(coll *Collection, setCode string, collectorNumber string) (*Card, error) {
sort := bson.D{{"_id", 1}}
searchFilter := bson.D{{"set", setCode}, {"collectornumber", collectorNumber}}
@ -251,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

@ -76,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

@ -26,7 +26,7 @@ var removeCmd = &cobra.Command{
RunE: func(cmd *cobra.Command, cards []string) error {
if interactive {
removeCardsInteractive(unique, set)
removeCardsInteractive(set)
} else {
removeCards(cards, count)
}
@ -34,7 +34,7 @@ var removeCmd = &cobra.Command{
},
}
func removeCardsInteractive(unique bool, set string) {
func removeCardsInteractive(set string) {
l := Logger()
if len(set) == 0 {
@ -103,6 +103,10 @@ func removeCards(cards []string, count int64) error {
continue
}
if !askConfirmation(c) {
continue
}
if foil && c.SerraCountFoil == 1 && c.SerraCount == 0 || !foil && c.SerraCount == 1 && c.SerraCountFoil == 0 {
coll.storageRemove(bson.M{"_id": c.ID})
l.Infof("\"%s\" (%.2f%s) removed", c.Name, c.getValue(foil), getCurrency())

View File

@ -1,3 +1,7 @@
// Package serra
//
// It implements base functions and also cli wrappers
// The entire tool consists only of this one package.
package serra
import (
@ -7,15 +11,19 @@ import (
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

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,12 +17,16 @@ 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"`
@ -119,18 +126,119 @@ type Card struct {
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 {
@ -170,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 {
@ -181,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
@ -191,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)
@ -204,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)
@ -220,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

@ -143,7 +143,7 @@ func ShowSet(setname string) error {
ri := convertRarities(rar)
fmt.Printf("%s\n", sets[0].Name)
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"])

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

@ -17,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
}

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,8 +19,8 @@ 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 {
@ -49,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)
@ -74,13 +98,13 @@ var updateCmd = &cobra.Command{
SaucerHead: "[green]>[reset]",
SaucerPadding: " ",
BarStart: "|",
BarEnd: set.Name,
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 {
l.Error(err)
continue
@ -115,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)
}
@ -130,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,64 +0,0 @@
package serra
import (
"encoding/json"
"fmt"
"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)
// 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 "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("cardmarketId,quantity,name,set,condition,language,isFoil,isPlayset,price,comment")
for _, card := range cards {
fmt.Printf("%.0f,%d,%s,%s,EX,German,false,false,%.2f,\n", card.CardmarketID, card.SerraCount, card.Name, card.SetName, card.getValue(false))
}
}
func exportJson(cards []Card) {
ehj, _ := json.MarshalIndent(cards, "", " ")
fmt.Println(string(ehj))
}

View File

@ -1,212 +0,0 @@
package serra
import (
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cobra"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
func init() {
rootCmd.AddCommand(statsCmd)
}
var statsCmd = &cobra.Command{
Aliases: []string{"stats"},
Use: "stats <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")}
l := Logger()
defer storageDisconnect(client)
// 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)
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)
// 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("\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)
}
// 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)
}
// Show cards added per month
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)
}
// 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
}
count_all, 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/count_all, getCurrency(), Reset)
total, _ := totalcoll.storageFindTotal()
fmt.Printf("History: \n")
showPriceHistory(total.Value, "* ", true)
return nil
},
}

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">