Simplify database schema for current use case

This commit is contained in:
The Magician 2024-04-24 11:54:49 +01:00
parent 424bca2d4d
commit d6e558f9e3
13 changed files with 35 additions and 691 deletions

View File

@ -1,8 +1,7 @@
removedb: removedb:
mysql --user=root --password=$(shell pass show sevenkeys/mysql) <sql/removedb.sql mysql --user=root --password=$(shell pass show sevenkeys/mysql) <sql/removedb.sql
createdb: createdb:
mysql --user=root --password=$(shell pass show sevenkeys/mysql) <sql/createdb.sql mysql --user=root --password=$(shell pass show sevenkeys/mysql) <sql/createdb.sql
dbup: createdb importdata: createdb
go run cmd/dbup/main.go
importdata: dbup
go run cmd/importdata/main.go go run cmd/importdata/main.go

View File

@ -1,10 +0,0 @@
package main
import (
"sevenkeys/database"
)
func main() {
db := database.GetDatabaseFromConfig("config.json")
database.CreateDatabaseSchema(db)
}

View File

@ -1,29 +1,11 @@
package main package main
import ( import (
"database/sql" "fmt"
"log"
"sevenkeys/database" "sevenkeys/database"
"sevenkeys/database/imports"
"sevenkeys/scryfall"
) )
func importSets(db *sql.DB) {
setList, err := scryfall.GetSets()
if err != nil {
log.Fatal(err)
}
err = imports.InsertSets(db, setList)
if err != nil {
log.Fatal(err)
}
}
func main() { func main() {
db := database.GetDatabaseFromConfig("config.json") db := database.GetDatabaseFromConfig("config.json")
//imports.InsertColors(db) fmt.Println(db)
//importSets(db)
// Import artists
// Import gamepieces and printings
} }

View File

@ -0,0 +1,9 @@
package database
type CardPrinting struct {
Id int
GamepieceId int
SetId [6]rune
FrontFaceImageUrl [2048]rune
BackFaceImageUrl [2048]rune
}

View File

@ -1,18 +0,0 @@
package entities
type ExpansionSet struct {
Id int
SetCode string
Name string
SetType string
ReleasedAt string
BlockCode string
Block string
ParentSetCode string
CardCount int
PrintedSize int
Digital bool
FoilOnly bool
NonfoilOnly bool
IconSvgUri string
}

View File

@ -0,0 +1,6 @@
package database
type Gamepiece struct {
Id int
Name string
}

View File

@ -1,64 +0,0 @@
package imports
import (
"database/sql"
"log"
)
func checkColorExists(db *sql.DB, abbrev string) bool {
query := "SELECT Id FROM Color WHERE Abbreviation = ?;"
var colorId int
row := db.QueryRow(query, abbrev)
err := row.Scan(&colorId)
if err == sql.ErrNoRows {
return false
} else if err != nil {
log.Fatal(err)
}
return true
}
func insertColor(db *sql.DB, color string, abbrev string) error {
query := `INSERT INTO Color (Name, Abbreviation) VALUES (?, ?);`
insert, err := db.Prepare(query)
defer insert.Close()
if err != nil {
return err
}
result, err := insert.Exec(color, abbrev)
rowsAffected, err := result.RowsAffected()
if err != nil || rowsAffected != 1 {
return err
}
return nil
}
func InsertColors(db *sql.DB) error {
colors := map[string]string{
"White": "W",
"Blue": "U",
"Black": "B",
"Red": "R",
"Green": "G",
}
for name, abbrev := range colors {
if checkColorExists(db, abbrev) {
log.Println("[Color] Skipping " + name + ", already in database")
continue
}
err := insertColor(db, name, abbrev)
if err != nil {
return err
}
}
return nil
}

View File

@ -1,59 +0,0 @@
package imports
import (
"database/sql"
"log"
"sevenkeys/scryfall"
)
func CheckSetExists(db *sql.DB, setCode string) bool {
query := "SELECT Id FROM ExpansionSet WHERE SetCode = ?;"
var setId int
row := db.QueryRow(query, setCode)
err := row.Scan(&setId)
if err == sql.ErrNoRows {
return false
} else if err != nil {
log.Fatal(err)
}
return true
}
func insertSet(db *sql.DB, set scryfall.Set) error {
query := "INSERT INTO ExpansionSet (SetCode, Name, SetType, ReleasedAt, BlockCode, Block, ParentSetCode, CardCount, PrintedSize, Digital, FoilOnly, NonfoilOnly, IconSvgUri) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"
insert, err := db.Prepare(query)
defer insert.Close()
if err != nil {
return err
}
if CheckSetExists(db, set.Code) {
log.Println("Skipping " + set.Code + ", already in database")
return nil
}
result, err := insert.Exec(set.Code, set.Name, set.SetType, set.ReleasedAt, set.BlockCode, set.Block, set.ParentSetCode, set.CardCount, set.PrintedSize, set.Digital, set.FoilOnly, set.NonfoilOnly, set.IconSvgUri)
rowsAffected, err := result.RowsAffected()
if err != nil || rowsAffected != 1 {
return err
}
return nil
}
func InsertSets(db *sql.DB, setList scryfall.SetList) error {
for index := range setList.Data {
set := setList.Data[index]
err := insertSet(db, set)
if err != nil {
return err
}
}
return nil
}

View File

@ -1 +0,0 @@
package imports

View File

@ -1,250 +0,0 @@
package database
import (
"database/sql"
"log"
)
func createGamepieceTable(db *sql.DB) {
query := `CREATE TABLE IF NOT EXISTS Gamepiece (
Id INT AUTO_INCREMENT PRIMARY KEY,
ArenaId INT NULL,
ScryfallId VARCHAR(36) NOT NULL,
OracleId VARCHAR(36) NOT NULL,
Name VARCHAR(141) NOT NULL,
Layout ENUM("normal",
"split",
"flip",
"transform",
"modal_dfc",
"meld",
"leveler",
"class",
"case",
"saga",
"adventure",
"mutate",
"prototype",
"battle",
"planar",
"scheme",
"vanguard",
"token",
"double_faced_token",
"emblem",
"augment",
"host",
"art_series",
"reversible_card") NOT NULL,
ManaCost VARCHAR(8) NOT NULL,
ManaValue FLOAT(7,2) NOT NULL,
TypeLine VARCHAR(50) NOT NULL,
OracleText VARCHAR(800) NOT NULL,
Power VARCHAR(5) NULL,
Toughness VARCHAR(5) NULL,
ReserveList BOOLEAN NOT NULL
);`
_, err := db.Exec(query)
if err != nil {
log.Fatal(err)
}
}
func createColorTable(db *sql.DB) {
query := `CREATE TABLE IF NOT EXISTS Color (
Id INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(5) NOT NULL,
Abbreviation VARCHAR(1) NOT NULL
);`
_, err := db.Exec(query)
if err != nil {
log.Fatal(err)
}
}
func createGamepieceColorTable(db *sql.DB) {
query := `CREATE TABLE IF NOT EXISTS GamepieceColor (
ColorId INT,
GamepieceId INT,
PRIMARY KEY (ColorId, GamepieceId),
FOREIGN KEY (ColorId) REFERENCES Color(Id),
FOREIGN KEY (GamepieceId) REFERENCES Gamepiece(Id)
);`
_, err := db.Exec(query)
if err != nil {
log.Fatal(err)
}
}
func createGamepieceColorIdentityTable(db *sql.DB) {
query := `CREATE TABLE IF NOT EXISTS GamepieceColorIdentity (
ColorId INT,
GamepieceId INT,
PRIMARY KEY (ColorId, GamepieceId),
FOREIGN KEY (ColorId) REFERENCES Color(Id),
FOREIGN KEY (GamepieceId) REFERENCES Gamepiece(Id)
);`
_, err := db.Exec(query)
if err != nil {
log.Fatal(err)
}
}
func createGamepieceColorIndicatorTable(db *sql.DB) {
query := `CREATE TABLE IF NOT EXISTS GamepieceColorIndicator (
ColorId INT,
GamepieceId INT,
PRIMARY KEY (ColorId, GamepieceId),
FOREIGN KEY (ColorId) REFERENCES Color(Id),
FOREIGN KEY (GamepieceId) REFERENCES Gamepiece(Id)
);`
_, err := db.Exec(query)
if err != nil {
log.Fatal(err)
}
}
func createKeywordTable(db *sql.DB) {
query := `CREATE TABLE IF NOT EXISTS Keyword (
Id INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(20) NOT NULL
);`
_, err := db.Exec(query)
if err != nil {
log.Fatal(err)
}
}
func createGamepieceKeywordTable(db *sql.DB) {
query := `CREATE TABLE IF NOT EXISTS GamepieceKeyword (
GamepieceId INT NOT NULL,
KeywordId INT NOT NULL,
PRIMARY KEY (GamepieceId, KeywordId),
FOREIGN KEY (GamepieceId) REFERENCES Gamepiece(Id),
FOREIGN KEY (KeywordId) REFERENCES Keyword(Id)
);`
_, err := db.Exec(query)
if err != nil {
log.Fatal(err)
}
}
func createFormatTable(db *sql.DB) {
query := `CREATE TABLE IF NOT EXISTS Format (
Id INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(50)
);`
_, err := db.Exec(query)
if err != nil {
log.Fatal(err)
}
}
func createGamepieceFormatLegalityTable(db *sql.DB) {
query := `CREATE TABLE IF NOT EXISTS GamepieceFormatLegality (
GamepieceId INT NOT NULL,
FormatId INT NOT NULL,
PRIMARY KEY (GamepieceId, FormatId),
FOREIGN KEY (GamepieceId) REFERENCES Gamepiece(Id),
FOREIGN KEY (FormatId) REFERENCES Format(Id),
Legality ENUM("legal", "not_legal", "restricted", "banned") NOT NULL
);`
_, err := db.Exec(query)
if err != nil {
log.Fatal(err)
}
}
func createSetTable(db *sql.DB) {
query := `CREATE TABLE IF NOT EXISTS ExpansionSet (
Id INT AUTO_INCREMENT PRIMARY KEY,
SetCode VARCHAR(6) NOT NULL,
Name VARCHAR(60) NOT NULL,
SetType VARCHAR(20) NOT NULL,
ReleasedAt DATETIME NOT NULL,
BlockCode VARCHAR(10) NULL,
Block VARCHAR(40) NULL,
ParentSetCode VARCHAR(5) NULL,
CardCount INT NOT NULL,
PrintedSize INT NULL,
Digital BOOLEAN NOT NULL,
FoilOnly BOOLEAN NOT NULL,
NonfoilOnly BOOLEAN NOT NULL,
IconSvgUri VARCHAR(60) NOT NULL
);`
_, err := db.Exec(query)
if err != nil {
log.Fatal(err)
}
}
func createArtistTable(db *sql.DB) {
query := `CREATE TABLE IF NOT EXISTS Artist (
Id INT AUTO_INCREMENT PRIMARY KEY,
ScryfallId VARCHAR(36) NOT NULL,
Name VARCHAR(100) NOT NULL
);`
_, err := db.Exec(query)
if err != nil {
log.Fatal(err)
}
}
func createCardPrintingTable(db *sql.DB) {
query := `CREATE TABLE IF NOT EXISTS CardPrinting (
Id INT AUTO_INCREMENT PRIMARY KEY,
GamepieceId INT,
FOREIGN KEY (GamepieceId) REFERENCES Gamepiece(Id),
Language VARCHAR(3) NOT NULL,
ReleasedAt DATETIME NOT NULL,
Reprint BOOLEAN NOT NULL,
ExpansionSetId INT NOT NULL,
FOREIGN KEY (ExpansionSetId) REFERENCES ExpansionSet(Id),
CollectorNumber VARCHAR(10) NOT NULL,
Rarity ENUM("common", "uncommon", "rare", "special", "mythic", "bonus") NOT NULL,
ArtistId INT NULL,
FOREIGN KEY (ArtistId) REFERENCES Artist(Id),
BorderColor ENUM("black", "white", "borderless", "silver", "gold") NOT NULL,
Frame ENUM("1993", "1997", "2003", "2015", "future") NOT NULL,
FullArt BOOLEAN NOT NULL,
Textless BOOLEAN NOT NULL,
StorySpotlight BOOLEAN NOT NULL,
FlavorName VARCHAR(40) NULL,
FlavorText VARCHAR(300) NULL,
DigitalOnly BOOLEAN NOT NULL,
Variation BOOLEAN NOT NULL,
VariationId VARCHAR(36) NULL,
SecurityStamp ENUM("oval", "triangle", "acorn", "circle", "arena", "heart") NULL
);`
_, err := db.Exec(query)
if err != nil {
log.Fatal(err)
}
}
func CreateDatabaseSchema(db *sql.DB) {
createGamepieceTable(db)
createColorTable(db)
createGamepieceColorTable(db)
createGamepieceColorIdentityTable(db)
createGamepieceColorIndicatorTable(db)
createKeywordTable(db)
createGamepieceKeywordTable(db)
createFormatTable(db)
createGamepieceFormatLegalityTable(db)
createSetTable(db)
createArtistTable(db)
createCardPrintingTable(db)
}

View File

@ -1,137 +0,0 @@
USE DATABASE sevenkeys;
CREATE TABLE IF NOT EXISTS Gamepiece (
Id INT AUTO_INCREMENT PRIMARY KEY,
ArenaId INT NULL,
ScryfallId VARCHAR(36) NOT NULL,
OracleId VARCHAR(36) NOT NULL,
Name VARCHAR(141) NOT NULL,
Layout ENUM("normal",
"split",
"flip",
"transform",
"modal_dfc",
"meld",
"leveler",
"class",
"case",
"saga",
"adventure",
"mutate",
"prototype",
"battle",
"planar",
"scheme",
"vanguard",
"token",
"double_faced_token",
"emblem",
"augment",
"host",
"art_series",
"reversible_card") NOT NULL,
ManaCost VARCHAR(8) NOT NULL,
ManaValue FLOAT(7,2) NOT NULL,
TypeLine VARCHAR(50) NOT NULL,
OracleText VARCHAR(800) NOT NULL,
Power VARCHAR(5) NULL,
Toughness VARCHAR(5) NULL,
ReserveList BOOLEAN NOT NULL
);
CREATE TABLE IF NOT EXISTS Color (
Id INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(5) NOT NULL,
Abbreviation VARCHAR(1) NOT NULL
);
CREATE TABLE IF NOT EXISTS GamepieceColor (
ColorId INT,
GamepieceId INT,
PRIMARY KEY (ColorId, GamepieceId),
FOREIGN KEY (ColorId) REFERENCES Color(Id),
FOREIGN KEY (GamepieceId) REFERENCES Gamepiece(Id)
);
CREATE TABLE IF NOT EXISTS GamepieceColorIdentity (
ColorId INT,
GamepieceId INT,
PRIMARY KEY (ColorId, GamepieceId),
FOREIGN KEY (ColorId) REFERENCES Color(Id),
FOREIGN KEY (GamepieceId) REFERENCES Gamepiece(Id)
);
CREATE TABLE IF NOT EXISTS GamepieceColorIndicator (
ColorId INT,
GamepieceId INT,
PRIMARY KEY (ColorId, GamepieceId),
FOREIGN KEY (ColorId) REFERENCES Color(Id),
FOREIGN KEY (GamepieceId) REFERENCES Gamepiece(Id)
);
CREATE TABLE IF NOT EXISTS Keyword (
Id INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(20) NOT NULL
);
CREATE TABLE IF NOT EXISTS GamepieceKeyword (
GamepieceId INT NOT NULL,
KeywordId INT NOT NULL,
PRIMARY KEY (GamepieceId, KeywordId),
FOREIGN KEY (GamepieceId) REFERENCES Gamepiece(Id),
FOREIGN KEY (KeywordId) REFERENCES Keyword(Id)
);
CREATE TABLE IF NOT EXISTS Format (
Id INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(50)
);
CREATE TABLE IF NOT EXISTS GamepieceFormatLegality (
GamepieceId INT NOT NULL,
FormatId INT NOT NULL,
PRIMARY KEY (GamepieceId, FormatId),
FOREIGN KEY (GamepieceId) REFERENCES Gamepiece(Id),
FOREIGN KEY (FormatId) REFERENCES Format(Id),
Legality ENUM("legal", "not_legal", "restricted", "banned") NOT NULL
);
CREATE TABLE IF NOT EXISTS ExpansionSet (
Id INT AUTO_INCREMENT NOT NULL,
SetCode VARCHAR(5) NOT NULL,
Name VARCHAR(40) NOT NULL,
Digital BOOLEAN NOT NULL,
SetType VARCHAR(20) NOT NULL
);
CREATE TABLE IF NOT EXISTS Artist (
Id INT AUTO_INCREMENT NOT NULL,
ScryfallId VARCHAR(36) NOT NULL,
Name VARCHAR(100) NOT NULL
);
CREATE TABLE IF NOT EXISTS CardPrinting (
Id INT AUTO_INCREMENT NOT NULL,
GamepieceId INT,
FOREIGN KEY (GamepieceId) REFERENCES Gamepiece(Id),
Language VARCHAR(3) NOT NULL,
ReleasedAt DATETIME NOT NULL,
Reprint BOOLEAN NOT NULL,
ExpansionSetId INT NOT NULL,
FOREIGN KEY (SetId) REFERENCES ExpansionSet(Id),
CollectorNumber VARCHAR(10) NOT NULL,
Rarity ENUM("common", "uncommon", "rare", "special", "mythic", "bonus") NOT NULL,
ArtistId INT NULL,
FOREIGN KEY (ArtistId) REFERENCES Artist(Id),
BorderColor ENUM("black", "white", "borderless", "silver", "gold") NOT NULL,
Frame ENUM("1993", "1997", "2003", "2015", "future") NOT NULL,
FullArt BOOLEAN NOT NULL,
Textless BOOLEAN NOT NULL,
StorySpotlight BOOLEAN NOT NULL,
FlavorName VARCHAR(40) NULL,
FlavorText VARCHAR(300) NULL,
DigitalOnly BOOLEAN NOT NULL,
Variation BOOLEAN NOT NULL,
VariationId VARCHAR(36) NULL,
SecurityStamp ENUM("oval", "triangle", "acorn", "circle", "arena", "heart") NULL
);

View File

@ -1 +1,17 @@
CREATE DATABASE IF NOT EXISTS sevenkeys; CREATE DATABASE IF NOT EXISTS sevenkeys;
USE sevenkeys;
CREATE TABLE IF NOT EXISTS Gamepiece (
Id INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(150) NOT NULL
);
CREATE TABLE IF NOT EXISTS CardPrinting (
Id INT AUTO_INCREMENT PRIMARY KEY,
GamepieceId INT NOT NULL,
FOREIGN KEY (GamepieceId) REFERENCES Gamepiece(Id),
SetId VARCHAR(6) NOT NULL,
FrontFaceImageUrl VARCHAR(2048) NOT NULL,
BackFaceImageUrl VARCHAR(2048) NOT NULL
);

View File

@ -1,129 +0,0 @@
{
"object": "card",
"id": "56ebc372-aabd-4174-a943-c7bf59e5028d",
"oracle_id": "e43e06fb-52b7-4f38-8fac-f31973b043f7",
"multiverse_ids": [
37113
],
"mtgo_id": 17622,
"mtgo_foil_id": 17623,
"tcgplayer_id": 10190,
"cardmarket_id": 2266,
"name": "Phantom Nishoba",
"lang": "en",
"released_at": "2002-05-27",
"uri": "https://api.scryfall.com/cards/56ebc372-aabd-4174-a943-c7bf59e5028d",
"scryfall_uri": "https://scryfall.com/card/jud/140/phantom-nishoba?utm_source=api",
"layout": "normal",
"highres_image": true,
"image_status": "highres_scan",
"image_uris": {
"small": "https://cards.scryfall.io/small/front/5/6/56ebc372-aabd-4174-a943-c7bf59e5028d.jpg?1562629953",
"normal": "https://cards.scryfall.io/normal/front/5/6/56ebc372-aabd-4174-a943-c7bf59e5028d.jpg?1562629953",
"large": "https://cards.scryfall.io/large/front/5/6/56ebc372-aabd-4174-a943-c7bf59e5028d.jpg?1562629953",
"png": "https://cards.scryfall.io/png/front/5/6/56ebc372-aabd-4174-a943-c7bf59e5028d.png?1562629953",
"art_crop": "https://cards.scryfall.io/art_crop/front/5/6/56ebc372-aabd-4174-a943-c7bf59e5028d.jpg?1562629953",
"border_crop": "https://cards.scryfall.io/border_crop/front/5/6/56ebc372-aabd-4174-a943-c7bf59e5028d.jpg?1562629953"
},
"mana_cost": "{5}{G}{W}",
"cmc": 7.0,
"type_line": "Creature — Cat Beast Spirit",
"oracle_text": "Trample\nPhantom Nishoba enters the battlefield with seven +1/+1 counters on it.\nWhenever Phantom Nishoba deals damage, you gain that much life.\nIf damage would be dealt to Phantom Nishoba, prevent that damage. Remove a +1/+1 counter from Phantom Nishoba.",
"power": "0",
"toughness": "0",
"colors": [
"G",
"W"
],
"color_identity": [
"G",
"W"
],
"keywords": [
"Trample"
],
"legalities": {
"standard": "not_legal",
"future": "not_legal",
"historic": "not_legal",
"timeless": "not_legal",
"gladiator": "not_legal",
"pioneer": "not_legal",
"explorer": "not_legal",
"modern": "not_legal",
"legacy": "legal",
"pauper": "not_legal",
"vintage": "legal",
"penny": "not_legal",
"commander": "legal",
"oathbreaker": "legal",
"standardbrawl": "not_legal",
"brawl": "not_legal",
"alchemy": "not_legal",
"paupercommander": "not_legal",
"duel": "legal",
"oldschool": "not_legal",
"premodern": "legal",
"predh": "legal"
},
"games": [
"paper",
"mtgo"
],
"reserved": false,
"foil": true,
"nonfoil": true,
"finishes": [
"nonfoil",
"foil"
],
"oversized": false,
"promo": false,
"reprint": false,
"variation": false,
"set_id": "cd82de1a-36fd-4618-bfe8-b45532a582d9",
"set": "jud",
"set_name": "Judgment",
"set_type": "expansion",
"set_uri": "https://api.scryfall.com/sets/cd82de1a-36fd-4618-bfe8-b45532a582d9",
"set_search_uri": "https://api.scryfall.com/cards/search?order=set&q=e%3Ajud&unique=prints",
"scryfall_set_uri": "https://scryfall.com/sets/jud?utm_source=api",
"rulings_uri": "https://api.scryfall.com/cards/56ebc372-aabd-4174-a943-c7bf59e5028d/rulings",
"prints_search_uri": "https://api.scryfall.com/cards/search?order=released&q=oracleid%3Ae43e06fb-52b7-4f38-8fac-f31973b043f7&unique=prints",
"collector_number": "140",
"digital": false,
"rarity": "rare",
"card_back_id": "0aeebaf5-8c7d-4636-9e82-8c27447861f7",
"artist": "Arnie Swekel",
"artist_ids": [
"af10ecf2-eb82-4100-97b2-6c236b0fa644"
],
"illustration_id": "2bdc53d2-1f4b-4f6d-b59d-985ff2a01268",
"border_color": "black",
"frame": "1997",
"full_art": false,
"textless": false,
"booster": true,
"story_spotlight": false,
"edhrec_rank": 9969,
"penny_rank": 4844,
"prices": {
"usd": "4.11",
"usd_foil": "20.26",
"usd_etched": null,
"eur": "4.04",
"eur_foil": "33.33",
"tix": "0.44"
},
"related_uris": {
"gatherer": "https://gatherer.wizards.com/Pages/Card/Details.aspx?multiverseid=37113&printed=false",
"tcgplayer_infinite_articles": "https://tcgplayer.pxf.io/c/4931599/1830156/21018?subId1=api&trafcat=infinite&u=https%3A%2F%2Finfinite.tcgplayer.com%2Fsearch%3FcontentMode%3Darticle%26game%3Dmagic%26partner%3Dscryfall%26q%3DPhantom%2BNishoba",
"tcgplayer_infinite_decks": "https://tcgplayer.pxf.io/c/4931599/1830156/21018?subId1=api&trafcat=infinite&u=https%3A%2F%2Finfinite.tcgplayer.com%2Fsearch%3FcontentMode%3Ddeck%26game%3Dmagic%26partner%3Dscryfall%26q%3DPhantom%2BNishoba",
"edhrec": "https://edhrec.com/route/?cc=Phantom+Nishoba"
},
"purchase_uris": {
"tcgplayer": "https://tcgplayer.pxf.io/c/4931599/1830156/21018?subId1=api&u=https%3A%2F%2Fwww.tcgplayer.com%2Fproduct%2F10190%3Fpage%3D1",
"cardmarket": "https://www.cardmarket.com/en/Magic/Products/Singles/Judgment/Phantom-Nishoba?referrer=scryfall&utm_campaign=card_prices&utm_medium=text&utm_source=scryfall",
"cardhoarder": "https://www.cardhoarder.com/cards/17622?affiliate_id=scryfall&ref=card-profile&utm_campaign=affiliate&utm_medium=card&utm_source=scryfall"
}
}