Skip to content

Commit

Permalink
Support for importing TCGPlayer mobile app CSV files.
Browse files Browse the repository at this point in the history
  • Loading branch information
AaronLS authored and NikolayXHD committed May 4, 2021
1 parent 4d87537 commit fdc046e
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 37 deletions.
10 changes: 10 additions & 0 deletions Mtgdb.Data/CardRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ public void Load()
CardPrintingsByName = CardsByName.ToDictionary(_ => _.Key, toPrintings, Str.Comparer);
TokenPrintingsByName = TokensByName.ToDictionary(_ => _.Key, toPrintings, Str.Comparer);

CardsAndTokensByTcgPlayerProductId = toTcgPlayerProductIdMap(Cards);

for (int i = 0; i < Cards.Count; i++)
{
var card = Cards[i];
Expand Down Expand Up @@ -249,6 +251,13 @@ Dictionary<string, List<Card>> toNamesakesMap(IEnumerable<Card> cards) =>
// card_by_name_sorting
gr => gr.OrderByDescending(_ => _.ReleaseDate).ToList(),
Str.Comparer);

Dictionary<int, List<Card>> toTcgPlayerProductIdMap(IEnumerable<Card> cards) =>
cards.GroupBy(_ => _.Identifiers.TcgPlayerProductId)
.ToDictionary(
gr => gr.Key,
gr => gr//.OrderByDescending(_ => _.ReleaseDate)
.ToList());
}

private void preProcessSet(Set set)
Expand Down Expand Up @@ -539,6 +548,7 @@ public IDictionary<string, IReadOnlyList<string>> MapPrintingsByName(bool tokens
public IDictionary<string, Card> CardsById { get; } = new Dictionary<string, Card>(Str.Comparer);

public IDictionary<string, List<Card>> CardsByName { get; private set; }
public IDictionary<int, List<Card>> CardsAndTokensByTcgPlayerProductId { get; private set; }
public IDictionary<string, List<Card>> TokensByName { get; private set; }
public IDictionary<string, HashSet<string>> CardIdsByName { get; private set; }
public IDictionary<string, HashSet<string>> TokenIdsByName { get; private set; }
Expand Down
7 changes: 7 additions & 0 deletions Mtgdb.Data/Model/Mtgjson/Card.cs
Original file line number Diff line number Diff line change
Expand Up @@ -749,5 +749,12 @@ public class CardIdentifiers
[JsonProperty("scryfallIllustrationId")]
[JsonConverter(typeof(InternedStringConverter))]
public string ScryfallIllustrationId { get; set; }


/// <summary>
/// Unique by printing, alt/extended art, promo. But NOT unique per foil. The foil/non-foil version have same product ID.
/// </summary>
[JsonProperty("tcgplayerProductId")]
public int TcgPlayerProductId { get; set; }
}
}
1 change: 1 addition & 0 deletions Mtgdb.Gui/DeckSerialization/DeckSerializationSubsystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public DeckSerializationSubsystem(
_formatters = new[]
{
new JsonDeckFormatter(),
new TcgCsvDeckFormatter(cardRepository),
new ForgeDeckFormatter(cardRepository, forgeSetRepo),
new MagarenaDeckFormatter(cardRepository),
new DeckedBuilderDeckFormatter(cardRepository),
Expand Down
129 changes: 129 additions & 0 deletions Mtgdb.Gui/DeckSerialization/TcgCsvDeckFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using FileHelpers;
using Mtgdb.Data;
using Mtgdb.Ui;
using NLog;

namespace Mtgdb.Gui
{

[DelimitedRecord(",")][IgnoreEmptyLines][IgnoreFirst]
public class CardCsvModel
{
public int Quantity {get;set;}

// E.g. Castle Locthwain (Extended Art), Faerie Guidemother (Showcase), "Goat // Food (16)"
public string Name {get;set;}

// E.g. Castle Locthwain
public string SimpleName {get;set;}
public string Set {get;set;}
public string CardNumber {get;set;}
public string SetCode {get;set;}
public string Printing {get;set;}
public string Condition {get;set;}
public string Language {get;set;}
public string Rarity {get;set;}
public int ProductID {get;set;}
public string SKU {get;set;}
}

/// <summary>
/// Supports importing the CSV format created by exporting a list from TCG Player's mobile app.
/// </summary>
public class TcgCsvDeckFormatter : IDeckFormatter
{
private FileHelperEngine<CardCsvModel> fileHelperEngine;

public TcgCsvDeckFormatter(CardRepository cardRepository)
{
fileHelperEngine = new FileHelperEngine<CardCsvModel>();
this.cardRepository = cardRepository;
}

public string Description => "TCGPlayer Mobile App CSV";
public string FileNamePattern => "*.csv";
public bool ValidateFormat(string serialized)
{
string header = serialized.Lines(StringSplitOptions.RemoveEmptyEntries).First();
var headers = header.Split(',');

// validate some of the more unique headers, to differentiate from other possible non-TCGPlayer CSV's
return headers.Contains("Simple Name") && headers.Contains("Product ID") && headers.Contains("SKU");
}

public bool SupportsExport => false;
public bool SupportsImport => true;
public bool SupportsFile => true;
public bool UseBom => false;
public string FormatHint => null;

private static readonly Logger _log = LogManager.GetCurrentClassLogger();
private readonly CardRepository cardRepository;

public virtual Deck ImportDeck(string serialized, bool exact = false)
{
var result = Deck.Create();

List<CardCsvModel> cards = fileHelperEngine.ReadStringAsList(serialized);

var unmatched = new HashSet<string>();

foreach (CardCsvModel csvCard in cards)
{
int count = csvCard.Quantity;
var card = GetCard(csvCard.SimpleName, csvCard.ProductID, csvCard.SetCode,csvCard.CardNumber);

if (card == null) {
unmatched.Add(csvCard.SimpleName);// error tracking
continue;
}

//if (isSideboard)
// add(card, count, result.Sideboard);
//else
add(card, count, result.MainDeck);

}

_log.Info("Unmatched cards:\r\n{0}", string.Join("\r\n", unmatched));

return result;
}


private static void add(Card card, int count, DeckZone collection)
{
if (collection.Count.ContainsKey(card.Id))
collection.Count[card.Id] += count;
else
{
collection.Count[card.Id] = count;
collection.Order.Add(card.Id);
}
}

public string ExportDeck(string name, Deck current, bool exact = false)
{
throw new NotImplementedException();
}

private Card GetCard(string cardName, int tcgPlayerProductId, string setCode= null, string cardNumber=null)
{
// Note TCGPlayer set codes are unreiable, there's some rogue codes like PPELD and PRE that don't exist mtgjson data models

// Instead, use TCG product ID.
//Unique by printing, alt / extended art, promo. But not by foil.
var cardVariants = cardRepository.CardsAndTokensByTcgPlayerProductId.TryGet(tcgPlayerProductId);

// TODO: Foil variant from the "Printing" CSV fiel?d which has values "Normal" or "Foil"

var card = cardVariants?.FirstOrDefault();
return card;
}

}
}
18 changes: 12 additions & 6 deletions Mtgdb.Gui/Mtgdb.Gui.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
</PropertyGroup>
<PropertyGroup />
<ItemGroup>
<Reference Include="FileHelpers, Version=3.4.2.0, Culture=neutral, PublicKeyToken=3e0c08d59cc3d657, processorArchitecture=MSIL">
<HintPath>..\packages\FileHelpers.3.4.2\lib\net45\FileHelpers.dll</HintPath>
</Reference>
<Reference Include="JetBrains.Annotations, Version=2020.1.0.0, Culture=neutral, PublicKeyToken=1010a0d8d6380325">
<HintPath>..\packages\JetBrains.Annotations.2020.1.0\lib\net20\JetBrains.Annotations.dll</HintPath>
<Private>True</Private>
Expand Down Expand Up @@ -109,6 +112,7 @@
<Compile Include="DeckSerialization\MtgArenaFormatter.cs" />
<Compile Include="DeckSerialization\MtgoDeckFormatter.cs" />
<Compile Include="DeckSerialization\RegexDeckFormatter.cs" />
<Compile Include="DeckSerialization\TcgCsvDeckFormatter.cs" />
<Compile Include="DeckSerialization\XitaxDeckTransformation.cs" />
<Compile Include="DeckSerialization\XMageDeckFormatter.cs" />
<Compile Include="FormChart\Aggregates.cs" />
Expand All @@ -118,7 +122,9 @@
<Compile Include="FormChart\DataElement.cs" />
<Compile Include="FormChart\DataSource.cs" />
<Compile Include="FormChart\CardFields.cs" />
<Compile Include="FormChart\FormChart.NPlot.cs" />
<Compile Include="FormChart\FormChart.NPlot.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="FormMain\CopyPasteSubsystem.cs" />
<Compile Include="FormMain\CountInputSubsystem.cs" />
<Compile Include="FormMain\DeckZoneSubsystem.cs" />
Expand All @@ -129,20 +135,20 @@
<Compile Include="FormMain\Evaluators.cs" />
<Compile Include="FormMain\FilterGroup.cs" />
<Compile Include="FormMain\FormMain.Definitions.cs">
<SubType>Form</SubType>
<SubType>UserControl</SubType>
</Compile>
<Compile Include="FormMain\FormMain.Designer.cs">
<DependentUpon>FormMain.cs</DependentUpon>
</Compile>
<Compile Include="DeckSerialization\IDeckFormatter.cs" />
<Compile Include="FormMain\FormMain.EventHandlers.cs">
<SubType>Form</SubType>
<SubType>UserControl</SubType>
</Compile>
<Compile Include="FormMain\FormMain.Scale.cs">
<SubType>Form</SubType>
<SubType>UserControl</SubType>
</Compile>
<Compile Include="FormMain\FormMain.Tooltip.cs">
<SubType>Form</SubType>
<SubType>UserControl</SubType>
</Compile>
<Compile Include="FormMain\IconRecognizerFactory.cs" />
<Compile Include="FormMain\ImagePreloadingSubsystem.cs" />
Expand Down Expand Up @@ -254,7 +260,7 @@
<DependentUpon>ResourcesType.resx</DependentUpon>
</Compile>
<Compile Include="FormMain\FormMain.cs">
<SubType>Form</SubType>
<SubType>UserControl</SubType>
</Compile>
<Compile Include="[ infrastructure ]\GuiProgram.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
Expand Down
1 change: 1 addition & 0 deletions Mtgdb.Gui/packages.config
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="FileHelpers" version="3.4.2" targetFramework="net461" />
<package id="JetBrains.Annotations" version="2020.1.0" targetFramework="net461" />
<package id="Newtonsoft.Json" version="12.0.3" targetFramework="net472" />
<package id="Ninject" version="3.3.4" targetFramework="net472" />
Expand Down
50 changes: 25 additions & 25 deletions Mtgdb.Util.Win/Mtgdb.Util.Win.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{E1349E1C-77A7-4FC3-BDA8-735CD5CB2735}</ProjectGuid>
<ProjectGuid>{40390DA2-253D-4143-97EA-FA61C1AFA6EA}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Mtgdb.Util</RootNamespace>
<AssemblyName>Mtgdb.Util.Win</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<LangVersion>latest</LangVersion>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
Expand Down Expand Up @@ -80,28 +80,28 @@
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<COMReference Include="Shell32">
<Guid>{50A7E9B0-70EF-11D1-B75A-00A0C90564FE}</Guid>
<VersionMajor>1</VersionMajor>
<VersionMinor>0</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>True</EmbedInteropTypes>
</COMReference>
</ItemGroup>
<ItemGroup>
<COMReference Include="IWshRuntimeLibrary">
<Guid>{F935DC20-1CF0-11D0-ADB9-00C04FD58A0B}</Guid>
<VersionMajor>1</VersionMajor>
<VersionMinor>0</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>True</EmbedInteropTypes>
</COMReference>
</ItemGroup>
<ItemGroup>
<COMReference Include="Shell32">
<Guid>{50A7E9B0-70EF-11D1-B75A-00A0C90564FE}</Guid>
<VersionMajor>1</VersionMajor>
<VersionMinor>0</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>True</EmbedInteropTypes>
</COMReference>
</ItemGroup>
<ItemGroup>
<COMReference Include="IWshRuntimeLibrary">
<Guid>{F935DC20-1CF0-11D0-ADB9-00C04FD58A0B}</Guid>
<VersionMajor>1</VersionMajor>
<VersionMinor>0</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>True</EmbedInteropTypes>
</COMReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Mtgdb.App.Localization\Mtgdb.App.Localization.csproj">
<Project>{10abce2d-9376-4f1e-b316-a8cc9805fad1}</Project>
Expand Down Expand Up @@ -141,4 +141,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>
19 changes: 14 additions & 5 deletions Mtgdb.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27130.2036
# Visual Studio Version 16
VisualStudioVersion = 16.0.31205.134
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mtgdb.Data", "Mtgdb.Data\Mtgdb.Data.csproj", "{4E1F0D65-B2B4-44DE-B2A7-F9F36521E475}"
EndProject
Expand Down Expand Up @@ -55,13 +55,22 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mtgdb.Util.Win", "Mtgdb.Uti
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
out\out.projitems*{37029487-6a56-4fa3-aa23-b2e46d5aa1b0}*SharedItemsImports = 13
shared\shared.projitems*{10abce2d-9376-4f1e-b316-a8cc9805fad1}*SharedItemsImports = 4
shared\shared.projitems*{1b2fea9b-3d4b-430f-a7d2-8cfe47820238}*SharedItemsImports = 4
shared\shared.projitems*{24c593f8-e50b-4765-aea9-b152c68ebdbc}*SharedItemsImports = 4
shared\shared.projitems*{3229ca82-875d-4154-92b7-2f7c47678010}*SharedItemsImports = 4
shared\shared.projitems*{40390da2-253d-4143-97ea-fa61c1afa6ea}*SharedItemsImports = 4
shared\shared.projitems*{4e1f0d65-b2b4-44de-b2a7-f9f36521e475}*SharedItemsImports = 4
shared\shared.projitems*{4fe226ac-ec61-451f-a602-c79da136ce25}*SharedItemsImports = 4
shared\shared.projitems*{65731f8b-3fd8-4893-b35f-371f69c9734d}*SharedItemsImports = 4
shared\shared.projitems*{6fac0808-416a-4605-a1e7-042f7a270bb3}*SharedItemsImports = 4
shared\shared.projitems*{97545c6d-acd6-4a2c-84dd-cd91293cfbb6}*SharedItemsImports = 4
tools\tools.projitems*{b1c7f4d9-46ba-4d18-92a0-2e8214973929}*SharedItemsImports = 13
shared\shared.projitems*{c2763956-008b-4e6b-8ac7-b634d8741c3b}*SharedItemsImports = 13
shared\shared.projitems*{c6c3c03b-b8bd-4208-b2da-727536b5cda1}*SharedItemsImports = 4
shared\shared.projitems*{c837c025-eb64-4e1a-85c4-306a88ed690f}*SharedItemsImports = 4
shared\shared.projitems*{d5c61885-5ef9-48fd-bb00-8b6622246ee5}*SharedItemsImports = 4
shared\shared.projitems*{e1349e1c-77a7-4fc3-bda8-735cd5cb2735}*SharedItemsImports = 4
shared\shared.projitems*{40390da2-253d-4143-97ea-fa61c1afa6ea}*SharedItemsImports = 4
shared\shared.projitems*{e6dc781a-1e0f-481b-aaa9-3585fbd0ffde}*SharedItemsImports = 4
EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down
13 changes: 13 additions & 0 deletions Test/Mtgdb.Util.Test/Dal/DeckFormattersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ public void Mtgo()
Log.Debug(name);
}

[TestCase(@"TestArtifacts\TcgCsvDeckFormatterInputTestCase.csv", 36)]
public void TcgPlayerCsv(string inputFilePath, int expectedCount)//, string expectedResultJsonFilePath)
{
var deckFormatter = new TcgCsvDeckFormatter(Repo);
// Test file to Parse. File generated by TCG Player mobile card scanning app:
string tcgCsvPath = Path.Combine(TestContext.CurrentContext.TestDirectory, inputFilePath);

Ui.Deck deck = deckFormatter.ImportDeck(File.ReadAllText(tcgCsvPath));

Assert.AreEqual(deck.MainDeck.Count.Sum(d=>d.Value), expectedCount);

}

private void findCards(FsPath decksLocation, RegexDeckFormatter formatter)
{
var matches = decksLocation.EnumerateFiles(formatter.FileNamePattern, SearchOption.AllDirectories)
Expand Down
Loading

0 comments on commit fdc046e

Please sign in to comment.