Skip to content

Commit

Permalink
New plugin for custom cards
Browse files Browse the repository at this point in the history
  • Loading branch information
jeandeaual committed May 19, 2020
1 parent 811babe commit 85840d0
Show file tree
Hide file tree
Showing 5 changed files with 335 additions and 3 deletions.
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,23 @@ Inspired by [decker](https://github.com/Splizard/decker) and [Frogtown](https://

* Import from the following websites:

* <https://en.cf-vanguard.com>
* <https://cf-vanguard.com>
* <https://en.cf-vanguard.com/deckrecipe>
* <https://cf-vanguard.com/deckrecipe>

* Custom cards

* You can create custom decks from a list of image URLs or local paths, using the format \
`<Count> <Image URL or path> (<Card name>)`:

```
1 https://example.com/cards/card1.png (Card Name 1)
4 https://example.com/cards/card2.png (Card Name 2)
2 C:\Users\User\Documents\Cards\card3.png (Card Name 3)
C:\Users\User\Documents\Cards\card4.png
```
`Count` is optional and defaults to 1, `Card name` is also optional.
This will create a deck composed of 1 `card1.png`, 4 `card2.png`, 2 `card3.png` and 1 `card4.png` (with no name).
* Available as a command-line application and a GUI (built using [Fyne](https://fyne.io/)).
Expand Down Expand Up @@ -138,7 +153,7 @@ Flags:
-format string
format of the deck (usually inferred from the input file name or URL, but required with stdin)
-mode string
available modes: mtg, pkm, ygo, cfv
available modes: mtg, pkm, ygo, cfv, custom
-name string
name of the deck (usually inferred from the input file name or URL, but required with stdin)
-option value
Expand All @@ -153,6 +168,7 @@ Flags:
cfv:
lang (enum): Language of the cards (default: en)
vanguard-first (bool): Put the first vanguard on top of the deck (default: true)
custom: no option available
-output string
destination folder (defaults to the current folder) (cannot be used with "-chest")
-template string
Expand Down
2 changes: 2 additions & 0 deletions plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"log"

"github.com/jeandeaual/tts-deckconverter/plugins"
"github.com/jeandeaual/tts-deckconverter/plugins/custom"
"github.com/jeandeaual/tts-deckconverter/plugins/mtg"
"github.com/jeandeaual/tts-deckconverter/plugins/pkm"
"github.com/jeandeaual/tts-deckconverter/plugins/vanguard"
Expand All @@ -19,6 +20,7 @@ func init() {
pkm.PokemonPlugin,
ygo.YGOPlugin,
vanguard.VanguardPlugin,
custom.CustomPlugin,
)

registerURLHandlers()
Expand Down
202 changes: 202 additions & 0 deletions plugins/custom/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package custom

import (
"bufio"
"io"
"regexp"
"strconv"
"strings"

"github.com/jeandeaual/tts-deckconverter/log"
"github.com/jeandeaual/tts-deckconverter/plugins"
)

var cardLineRegexps = []*regexp.Regexp{
regexp.MustCompile(`^\s*(?:(?P<Count>\d+)x?\s+)?(?P<Path>.+)\s+\((?P<Name>.+)\)$`),
regexp.MustCompile(`^\s*(?:(?P<Count>\d+)x?\s+)?(?P<Path>.+)$`),
}

// CardInfo contains a card file path and its name.
type CardInfo struct {
// Name of the card.
Path string
// Set the card belongs to.
Name *string
}

// CardFiles contains the card file paths and their count.
type CardFiles struct {
// Cards are the card names.
Cards []CardInfo
// Counts is a map of card name to count (number of this card in the deck).
Counts map[string]int
}

// NewCardNames creates a new CardFiles struct.
func NewCardNames() *CardFiles {
counts := make(map[string]int)
return &CardFiles{Counts: counts}
}

// Insert a new card in a CardFiles struct.
func (c *CardFiles) Insert(path string, name *string) {
c.InsertCount(path, name, 1)
}

// InsertCount inserts several new cards in a CardFiles struct.
func (c *CardFiles) InsertCount(path string, name *string, count int) {
_, found := c.Counts[path]
if !found {
c.Cards = append(c.Cards, CardInfo{
Path: path,
Name: name,
})
c.Counts[path] = count
} else {
c.Counts[path] = c.Counts[path] + count
}
}

// String representation of a CardFiles struct.
func (c *CardFiles) String() string {
var sb strings.Builder

for _, cardInfo := range c.Cards {
count := c.Counts[cardInfo.Path]
sb.WriteString(strconv.Itoa(count))
sb.WriteString(" ")
sb.WriteString(cardInfo.Path)
if cardInfo.Name != nil {
sb.WriteString("(")
sb.WriteString(*cardInfo.Name)
sb.WriteString(")")
}
sb.WriteString("\n")
}

return sb.String()
}

func cardFilesToDeck(cards *CardFiles, name string, options map[string]interface{}) (*plugins.Deck, error) {
deck := &plugins.Deck{
Name: name,
CardSize: plugins.CardSizeStandard,
}

for _, cardInfo := range cards.Cards {
card := plugins.CardInfo{
ImageURL: cardInfo.Path,
Count: cards.Counts[cardInfo.Path],
}
if cardInfo.Name != nil {
card.Name = *cardInfo.Name
}
deck.Cards = append(deck.Cards, card)
}

return deck, nil
}

func fromList(file io.Reader, name string, options map[string]string) ([]*plugins.Deck, error) {
// Check the options
validatedOptions, err := CustomPlugin.AvailableOptions().ValidateNormalize(options)
if err != nil {
return nil, err
}

main, err := parseList(file)
if err != nil {
return nil, err
}

var decks []*plugins.Deck

if main != nil {
deck, err := cardFilesToDeck(main, name, validatedOptions)
if err != nil {
return nil, err
}

decks = append(decks, deck)
}

return decks, nil
}

func parseList(file io.Reader) (*CardFiles, error) {
var main *CardFiles
scanner := bufio.NewScanner(file)

for scanner.Scan() {
line := scanner.Text()

if strings.HasPrefix(line, "//") {
// Comment, ignore
continue
}

// Try to parse the line
for _, regex := range cardLineRegexps {
matches := regex.FindStringSubmatch(line)
if matches == nil {
continue
}

groupNames := regex.SubexpNames()

count := 1
countIdx := plugins.IndexOf("Count", groupNames)
if countIdx != -1 && len(matches[countIdx]) > 0 {
var err error
count, err = strconv.Atoi(matches[countIdx])
if err != nil {
log.Errorf("Error when parsing count: %s", err)
continue
}
}

pathIdx := plugins.IndexOf("Path", groupNames)
if pathIdx == -1 {
log.Errorf("path not present in regex: %s", regex)
continue
}
path := matches[pathIdx]

var name *string
nameIdx := plugins.IndexOf("Name", groupNames)
if nameIdx != -1 {
name = &matches[nameIdx]
}

log.Debugw(
"Found card",
"path", path,
"count", count,
"name", name,
"regex", regex,
"matches", matches,
"groupNames", groupNames,
)

if main == nil {
main = NewCardNames()
}
main.InsertCount(path, name, count)

break
}
}

if main != nil {
log.Debugf("Main: %d different card(s)\n%v", len(main.Cards), main)
} else {
log.Debug("Main: 0 cards")
}

if err := scanner.Err(); err != nil {
log.Error(err)
return main, err
}

return main, nil
}
64 changes: 64 additions & 0 deletions plugins/custom/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package custom

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"go.uber.org/zap"

"github.com/jeandeaual/tts-deckconverter/log"
)

func init() {
logger := zap.NewExample()
log.SetLogger(logger.Sugar())
}

func TestParseDeckFile(t *testing.T) {
deck, err := parseList(strings.NewReader(""))
assert.Nil(t, deck)
assert.Nil(t, err)

deck, err = parseList(
strings.NewReader(`// Deck
1 https://img.scryfall.com/cards/large/front/7/3/732fa4c9-11da-4bdb-96af-aa37c74be25f.jpg?1562803341
https://img.scryfall.com/cards/large/front/f/3/f3dd4d92-6471-4f4b-9c70-cbd2196e8c7b.jpg?1562640130 (Horse)
/home/user/test.jpg (Test 1)
3 C:\Users\User\Test\Test Card.jpg (Test 2)`),
)
expectedNames := []string{
"Horse",
"Test 1",
"Test 2",
}
expected := &CardFiles{
Cards: []CardInfo{
{
Path: "https://img.scryfall.com/cards/large/front/7/3/732fa4c9-11da-4bdb-96af-aa37c74be25f.jpg?1562803341",
Name: nil,
},
{
Path: "https://img.scryfall.com/cards/large/front/f/3/f3dd4d92-6471-4f4b-9c70-cbd2196e8c7b.jpg?1562640130",
Name: &expectedNames[0],
},
{
Path: "/home/user/test.jpg",
Name: &expectedNames[1],
},
{
Path: "C:\\Users\\User\\Test\\Test Card.jpg",
Name: &expectedNames[2],
},
},
Counts: map[string]int{
"https://img.scryfall.com/cards/large/front/7/3/732fa4c9-11da-4bdb-96af-aa37c74be25f.jpg?1562803341": 1,
"https://img.scryfall.com/cards/large/front/f/3/f3dd4d92-6471-4f4b-9c70-cbd2196e8c7b.jpg?1562640130": 1,
"/home/user/test.jpg": 1,
"C:\\Users\\User\\Test\\Test Card.jpg": 3,
},
}

assert.Equal(t, expected, deck)
assert.Nil(t, err)
}
48 changes: 48 additions & 0 deletions plugins/custom/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package custom

import (
"github.com/jeandeaual/tts-deckconverter/plugins"
)

type customPlugin struct {
id string
name string
}

func (p customPlugin) PluginID() string {
return p.id
}

func (p customPlugin) PluginName() string {
return p.name
}

func (p customPlugin) AvailableOptions() plugins.Options {
return plugins.Options{}
}

func (p customPlugin) URLHandlers() []plugins.URLHandler {
return []plugins.URLHandler{}
}

func (p customPlugin) FileExtHandlers() map[string]plugins.FileHandler {
return map[string]plugins.FileHandler{}
}

func (p customPlugin) DeckTypeHandlers() map[string]plugins.FileHandler {
return map[string]plugins.FileHandler{}
}

func (p customPlugin) GenericFileHandler() plugins.FileHandler {
return fromList
}

func (p customPlugin) AvailableBacks() map[string]plugins.Back {
return map[string]plugins.Back{}
}

// CustomPlugin is the exported plugin for this package
var CustomPlugin = customPlugin{
id: "custom",
name: "Custom",
}

0 comments on commit 85840d0

Please sign in to comment.