Compare commits
40 Commits
67dc528c6a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d21571561 | |||
| 821d301de3 | |||
| 6b5034c544 | |||
| 43a75c0b92 | |||
| 130287caa7 | |||
| 3df07d7622 | |||
| fd358f5d3f | |||
| f1beae9198 | |||
| 4dde40b72e | |||
| a21520b5e5 | |||
| af1ac9b5fa | |||
| 9f9a0b1fb7 | |||
| 3e1b89312a | |||
| 3c78637809 | |||
| e40a64579b | |||
| e966a22707 | |||
| 3f7cd6353a | |||
| 6ff0204189 | |||
| 9a8c971d73 | |||
| b6664492fa | |||
| bcf68c8332 | |||
| 3f4af21a93 | |||
| 98af8885a6 | |||
| 9f03e3e11f | |||
| 9a9f42bc1e | |||
| 6558a31619 | |||
| 72fa35d41a | |||
| ff4c58113f | |||
| da121940da | |||
| 9c2d9c1fb7 | |||
| 1462401787 | |||
| 5dfdff17c1 | |||
| 4f095c55ec | |||
| d168ca88ec | |||
| 6d21f5e496 | |||
| 906aaa1e59 | |||
| 6b4105ecd9 | |||
| 9f1eac75a0 | |||
| 16be88c703 | |||
| 7786cac7f5 |
@@ -1,16 +1,16 @@
|
|||||||
use thiserror::Error;
|
use rand::rng;
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
|
use std::fmt;
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
use strum_macros::EnumIter;
|
use strum_macros::EnumIter;
|
||||||
use std::fmt;
|
use thiserror::Error;
|
||||||
use rand::seq::SliceRandom;
|
|
||||||
use rand::rng;
|
|
||||||
|
|
||||||
#[derive(PartialEq, Debug, EnumIter, Copy, Clone)]
|
#[derive(PartialEq, Debug, EnumIter, Copy, Clone)]
|
||||||
pub enum Suit {
|
pub enum Suit {
|
||||||
Heart,
|
Heart,
|
||||||
Diamond,
|
Diamond,
|
||||||
Club,
|
Club,
|
||||||
Spade
|
Spade,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
@@ -53,7 +53,7 @@ pub enum Value {
|
|||||||
Ten,
|
Ten,
|
||||||
Jack,
|
Jack,
|
||||||
Queen,
|
Queen,
|
||||||
King
|
King,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Value {
|
impl Value {
|
||||||
@@ -97,7 +97,6 @@ impl fmt::Display for Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||||
pub struct Card {
|
pub struct Card {
|
||||||
pub suit: Suit,
|
pub suit: Suit,
|
||||||
@@ -105,7 +104,6 @@ pub struct Card {
|
|||||||
pub visible: bool,
|
pub visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl fmt::Display for Card {
|
impl fmt::Display for Card {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}{}", self.value, self.suit)
|
write!(f, "{}{}", self.value, self.suit)
|
||||||
@@ -140,8 +138,13 @@ impl Card {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Needs to be adjacent
|
// Needs to be adjacent
|
||||||
if self.value == Value::King || self.value.indexed_values() + 1 != top.value.indexed_values() {
|
if self.value == Value::King
|
||||||
return Err(StackingError::NotAdjacent(self.to_string(), top.to_string()));
|
|| self.value.indexed_values() + 1 != top.value.indexed_values()
|
||||||
|
{
|
||||||
|
return Err(StackingError::NotAdjacent(
|
||||||
|
self.to_string(),
|
||||||
|
top.to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -153,9 +156,12 @@ impl Card {
|
|||||||
if self.value == Value::Ace {
|
if self.value == Value::Ace {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else {
|
} else {
|
||||||
return Err(StackingError::NotAdjacent(self.to_string(), "an empty foundation".to_string()));
|
return Err(StackingError::NotAdjacent(
|
||||||
|
self.to_string(),
|
||||||
|
"an empty foundation".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Some(c) => {
|
Some(c) => {
|
||||||
if self.suit != c.suit {
|
if self.suit != c.suit {
|
||||||
return Err(StackingError::WrongSuit);
|
return Err(StackingError::WrongSuit);
|
||||||
@@ -172,7 +178,7 @@ impl Card {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Deck {
|
pub struct Deck {
|
||||||
pub cards: Vec<Card>
|
pub cards: Vec<Card>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Deck {
|
impl Default for Deck {
|
||||||
@@ -180,18 +186,14 @@ impl Default for Deck {
|
|||||||
let mut array = Vec::new();
|
let mut array = Vec::new();
|
||||||
for suit in Suit::iter() {
|
for suit in Suit::iter() {
|
||||||
for value in Value::iter() {
|
for value in Value::iter() {
|
||||||
array.push(
|
array.push(Card {
|
||||||
Card {
|
suit,
|
||||||
suit,
|
value,
|
||||||
value,
|
..Default::default()
|
||||||
..Default::default()
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Deck {
|
Deck { cards: array }
|
||||||
cards: array,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +224,7 @@ pub struct CardAndPosition {
|
|||||||
pub enum CardPosition {
|
pub enum CardPosition {
|
||||||
TopWaste, // I don't think we'd ever interact with anything other than the top of the Waste
|
TopWaste, // I don't think we'd ever interact with anything other than the top of the Waste
|
||||||
Pile(usize, usize), // (PileNumber, Index)
|
Pile(usize, usize), // (PileNumber, Index)
|
||||||
Foundation(usize)
|
Foundation(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
@@ -245,7 +247,7 @@ pub struct Klondike {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Klondike {
|
impl Klondike {
|
||||||
pub fn deck_to_waste(self: &mut Self) {
|
pub fn deck_to_waste(&mut self) {
|
||||||
for _ in 0..self.num_cards_turned {
|
for _ in 0..self.num_cards_turned {
|
||||||
let card = self.deck.pop();
|
let card = self.deck.pop();
|
||||||
match card {
|
match card {
|
||||||
@@ -255,13 +257,15 @@ impl Klondike {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO return some sort of error
|
// TODO return some sort of error
|
||||||
pub fn waste_to_deck(self: &mut Self) {
|
pub fn waste_to_deck(&mut self) {
|
||||||
match self.max_num_passes_through_deck {
|
match self.max_num_passes_through_deck {
|
||||||
NumPassesThroughDeck::Unlimited => (),
|
NumPassesThroughDeck::Unlimited => (),
|
||||||
NumPassesThroughDeck::Limited(n) => if n >= self.current_num_passes_through_deck {
|
NumPassesThroughDeck::Limited(n) => {
|
||||||
// no!
|
if n >= self.current_num_passes_through_deck {
|
||||||
return
|
// no!
|
||||||
},
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if self.deck.len() != 0 {
|
if self.deck.len() != 0 {
|
||||||
// no!
|
// no!
|
||||||
@@ -275,7 +279,7 @@ impl Klondike {
|
|||||||
pub fn move_card(self, source_card: &CardAndPosition, dest_card: &CardAndPosition) -> bool {
|
pub fn move_card(self, source_card: &CardAndPosition, dest_card: &CardAndPosition) -> bool {
|
||||||
// TODO raise errors properly
|
// TODO raise errors properly
|
||||||
assert!(source_card.card.is_some());
|
assert!(source_card.card.is_some());
|
||||||
assert_eq!(source_card.card.unwrap().visible, true);
|
assert!(source_card.card.unwrap().visible);
|
||||||
dest_card.pos.is_valid_dest();
|
dest_card.pos.is_valid_dest();
|
||||||
|
|
||||||
// Maybe TODO - check the .cards is the actual card in that position
|
// Maybe TODO - check the .cards is the actual card in that position
|
||||||
@@ -283,19 +287,30 @@ impl Klondike {
|
|||||||
match dest_card.pos {
|
match dest_card.pos {
|
||||||
CardPosition::Pile(_, _) => self.move_card_to_pile(&source_card, &dest_card),
|
CardPosition::Pile(_, _) => self.move_card_to_pile(&source_card, &dest_card),
|
||||||
CardPosition::Foundation(_f) => self.move_card_to_foundation(source_card, dest_card),
|
CardPosition::Foundation(_f) => self.move_card_to_foundation(source_card, dest_card),
|
||||||
CardPosition::TopWaste => unreachable!()
|
CardPosition::TopWaste => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_card_to_foundation(mut self, source_card: &CardAndPosition, dest_card: &CardAndPosition) -> bool {
|
pub fn move_card_to_foundation(
|
||||||
|
mut self,
|
||||||
|
source_card: &CardAndPosition,
|
||||||
|
dest_card: &CardAndPosition,
|
||||||
|
) -> bool {
|
||||||
// TODO check the cards referenced in source and dest are the ones that are actually there
|
// TODO check the cards referenced in source and dest are the ones that are actually there
|
||||||
// TODO - ditto this for the "move to pile" function
|
// TODO - ditto this for the "move to pile" function
|
||||||
// TODO actually learn Rust properly so I can figure out why I need to clone the whole struct to check a value
|
// TODO actually learn Rust properly so I can figure out why I need to clone the whole struct to check a value
|
||||||
if source_card.pos != CardPosition::TopWaste && !self.clone().is_card_top_of_pile(&source_card.pos) {
|
if source_card.pos != CardPosition::TopWaste
|
||||||
|
&& !self.clone().is_card_top_of_pile(&source_card.pos)
|
||||||
|
{
|
||||||
// TODO as above - make all these proper errors
|
// TODO as above - make all these proper errors
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if source_card.card.unwrap().can_be_placed_on_foundation(&dest_card.card).is_err() {
|
if source_card
|
||||||
|
.card
|
||||||
|
.unwrap()
|
||||||
|
.can_be_placed_on_foundation(&dest_card.card)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,27 +320,36 @@ impl Klondike {
|
|||||||
let card = self.waste.pop().unwrap();
|
let card = self.waste.pop().unwrap();
|
||||||
self.foundation[foundation_index].push(card);
|
self.foundation[foundation_index].push(card);
|
||||||
return true;
|
return true;
|
||||||
},
|
}
|
||||||
CardPosition::Pile(pile_index, _) => {
|
CardPosition::Pile(pile_index, _) => {
|
||||||
let card = self.piles[pile_index].pop().unwrap();
|
let card = self.piles[pile_index].pop().unwrap();
|
||||||
self.foundation[foundation_index].push(card);
|
self.foundation[foundation_index].push(card);
|
||||||
return true;
|
return true;
|
||||||
},
|
}
|
||||||
CardPosition::Foundation(_) => {
|
CardPosition::Foundation(_) => {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
unreachable!();
|
unreachable!();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_card_to_pile(mut self, source_card: &CardAndPosition, dest_card: &CardAndPosition) -> bool {
|
pub fn move_card_to_pile(
|
||||||
if source_card.card.unwrap().can_be_placed_on_pile(&dest_card.card.unwrap()).is_err() {
|
mut self,
|
||||||
|
source_card: &CardAndPosition,
|
||||||
|
dest_card: &CardAndPosition,
|
||||||
|
) -> bool {
|
||||||
|
if source_card
|
||||||
|
.card
|
||||||
|
.unwrap()
|
||||||
|
.can_be_placed_on_pile(&dest_card.card.unwrap())
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let (dpile_index, dcard_index) = match dest_card.pos {
|
let (dpile_index, dcard_index) = match dest_card.pos {
|
||||||
CardPosition::Pile(p, i) => (p, i),
|
CardPosition::Pile(p, i) => (p, i),
|
||||||
CardPosition::TopWaste | CardPosition::Foundation(_) => return false
|
CardPosition::TopWaste | CardPosition::Foundation(_) => return false,
|
||||||
};
|
};
|
||||||
if dcard_index != self.piles[dpile_index].len() - 1 {
|
if dcard_index != self.piles[dpile_index].len() - 1 {
|
||||||
// Can't move to anything other than top of pile
|
// Can't move to anything other than top of pile
|
||||||
@@ -337,7 +361,7 @@ impl Klondike {
|
|||||||
CardPosition::TopWaste => {
|
CardPosition::TopWaste => {
|
||||||
let card = self.waste.pop().unwrap();
|
let card = self.waste.pop().unwrap();
|
||||||
self.piles[dpile_index].push(card);
|
self.piles[dpile_index].push(card);
|
||||||
},
|
}
|
||||||
CardPosition::Pile(spile_index, scard_index) => {
|
CardPosition::Pile(spile_index, scard_index) => {
|
||||||
let num_cards_to_take = self.piles[spile_index].len() - scard_index; // -1 maybe?
|
let num_cards_to_take = self.piles[spile_index].len() - scard_index; // -1 maybe?
|
||||||
let mut cards: Vec<Card> = Vec::new();
|
let mut cards: Vec<Card> = Vec::new();
|
||||||
@@ -349,11 +373,11 @@ impl Klondike {
|
|||||||
}
|
}
|
||||||
// TODO Properly learn rust and why I can't use drain & extend methods
|
// TODO Properly learn rust and why I can't use drain & extend methods
|
||||||
// https://doc.rust-lang.org/std/vec/struct.Vec.html#method.drain
|
// https://doc.rust-lang.org/std/vec/struct.Vec.html#method.drain
|
||||||
},
|
}
|
||||||
CardPosition::Foundation(s_index) => {
|
CardPosition::Foundation(s_index) => {
|
||||||
let card = self.foundation[s_index].pop().unwrap();
|
let card = self.foundation[s_index].pop().unwrap();
|
||||||
self.piles[dpile_index].push(card);
|
self.piles[dpile_index].push(card);
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -361,21 +385,31 @@ impl Klondike {
|
|||||||
fn is_card_top_of_pile(self, pos: &CardPosition) -> bool {
|
fn is_card_top_of_pile(self, pos: &CardPosition) -> bool {
|
||||||
// TODO consider, at which point the Pos::Pile() ranges etc are correct
|
// TODO consider, at which point the Pos::Pile() ranges etc are correct
|
||||||
match pos {
|
match pos {
|
||||||
CardPosition::Pile(pile_index, card_index) => {
|
CardPosition::Pile(pile_index, card_index) => match self.piles.get(*pile_index) {
|
||||||
match self.piles.get(*pile_index) {
|
Some(pile_index) => {
|
||||||
Some(pile_index) => {
|
if *card_index == (pile_index.len() - 1) {
|
||||||
if *card_index == (pile_index.len() - 1) {
|
true
|
||||||
true
|
} else {
|
||||||
} else {
|
false
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None => false
|
|
||||||
}
|
}
|
||||||
|
None => false,
|
||||||
},
|
},
|
||||||
CardPosition::TopWaste | CardPosition::Foundation(_) => false
|
CardPosition::TopWaste | CardPosition::Foundation(_) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn lowest_visible_card_in_pile_from_index(self, pile: usize, index: usize) -> usize {
|
||||||
|
if self.piles[pile].len() == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
for (i, card) in self.piles[pile].iter().skip(index).enumerate() {
|
||||||
|
if card.visible {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic!("Pile with only facedown cards - this is wrong")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CardPosition {
|
impl CardPosition {
|
||||||
@@ -392,9 +426,17 @@ impl Default for Klondike {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let mut deck = Deck::default();
|
let mut deck = Deck::default();
|
||||||
deck.shuffle(Seed::Unseeded);
|
deck.shuffle(Seed::Unseeded);
|
||||||
let mut piles: [Vec<Card>; NUM_PILES_KLONDIKE] = [Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new()];
|
let mut piles: [Vec<Card>; NUM_PILES_KLONDIKE] = [
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
];
|
||||||
for pile in 0..NUM_PILES_KLONDIKE {
|
for pile in 0..NUM_PILES_KLONDIKE {
|
||||||
for num in 0..pile+1 {
|
for num in 0..pile + 1 {
|
||||||
let mut c = deck.cards.pop().unwrap();
|
let mut c = deck.cards.pop().unwrap();
|
||||||
if num == pile {
|
if num == pile {
|
||||||
c.visible = true;
|
c.visible = true;
|
||||||
@@ -433,19 +475,28 @@ mod tests {
|
|||||||
value: Value::Six,
|
value: Value::Six,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
assert_eq!(testing_card.can_be_placed_on_pile(&bad_same_suit), Err(StackingError::SameColour));
|
assert_eq!(
|
||||||
|
testing_card.can_be_placed_on_pile(&bad_same_suit),
|
||||||
|
Err(StackingError::SameColour)
|
||||||
|
);
|
||||||
let bad_same_colour = Card {
|
let bad_same_colour = Card {
|
||||||
suit: Suit::Diamond,
|
suit: Suit::Diamond,
|
||||||
value: Value::Six,
|
value: Value::Six,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
assert_eq!(testing_card.can_be_placed_on_pile(&bad_same_colour), Err(StackingError::SameColour));
|
assert_eq!(
|
||||||
|
testing_card.can_be_placed_on_pile(&bad_same_colour),
|
||||||
|
Err(StackingError::SameColour)
|
||||||
|
);
|
||||||
let should_stack_card = Card {
|
let should_stack_card = Card {
|
||||||
suit: Suit::Club,
|
suit: Suit::Club,
|
||||||
value: Value::Six,
|
value: Value::Six,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
assert_eq!(testing_card.can_be_placed_on_pile(&should_stack_card), Ok(()));
|
assert_eq!(
|
||||||
|
testing_card.can_be_placed_on_pile(&should_stack_card),
|
||||||
|
Ok(())
|
||||||
|
);
|
||||||
let value_too_high = Card {
|
let value_too_high = Card {
|
||||||
suit: Suit::Club,
|
suit: Suit::Club,
|
||||||
value: Value::Seven,
|
value: Value::Seven,
|
||||||
@@ -455,7 +506,9 @@ mod tests {
|
|||||||
if let Err(e) = not_adj_error {
|
if let Err(e) = not_adj_error {
|
||||||
match e {
|
match e {
|
||||||
StackingError::NotAdjacent(_, _) => assert!(true),
|
StackingError::NotAdjacent(_, _) => assert!(true),
|
||||||
StackingError::SameColour => assert!(false, "Colour is different - incorrect error"),
|
StackingError::SameColour => {
|
||||||
|
assert!(false, "Colour is different - incorrect error")
|
||||||
|
}
|
||||||
StackingError::WrongSuit => unreachable!(),
|
StackingError::WrongSuit => unreachable!(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -486,12 +539,12 @@ mod tests {
|
|||||||
let ace = Card {
|
let ace = Card {
|
||||||
suit: Suit::Spade,
|
suit: Suit::Spade,
|
||||||
value: Value::Ace,
|
value: Value::Ace,
|
||||||
.. Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
klon.piles[0].push(ace.clone());
|
klon.piles[0].push(ace.clone());
|
||||||
let source_card = CardAndPosition {
|
let source_card = CardAndPosition {
|
||||||
card: Some(ace.clone()),
|
card: Some(ace.clone()),
|
||||||
pos: CardPosition::Pile(0, 1)
|
pos: CardPosition::Pile(0, 1),
|
||||||
};
|
};
|
||||||
let dest_card = CardAndPosition {
|
let dest_card = CardAndPosition {
|
||||||
card: None,
|
card: None,
|
||||||
@@ -504,12 +557,12 @@ mod tests {
|
|||||||
let two = Card {
|
let two = Card {
|
||||||
suit: Suit::Spade,
|
suit: Suit::Spade,
|
||||||
value: Value::Two,
|
value: Value::Two,
|
||||||
.. Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
klon.piles[0].push(two.clone());
|
klon.piles[0].push(two.clone());
|
||||||
let source_card = CardAndPosition {
|
let source_card = CardAndPosition {
|
||||||
card: Some(two.clone()),
|
card: Some(two.clone()),
|
||||||
pos: CardPosition::Pile(0, 1)
|
pos: CardPosition::Pile(0, 1),
|
||||||
};
|
};
|
||||||
let dest_card = CardAndPosition {
|
let dest_card = CardAndPosition {
|
||||||
card: None,
|
card: None,
|
||||||
@@ -522,7 +575,7 @@ mod tests {
|
|||||||
let ace = Card {
|
let ace = Card {
|
||||||
suit: Suit::Spade,
|
suit: Suit::Spade,
|
||||||
value: Value::Ace,
|
value: Value::Ace,
|
||||||
.. Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
klon.waste.push(ace.clone());
|
klon.waste.push(ace.clone());
|
||||||
let source_card = CardAndPosition {
|
let source_card = CardAndPosition {
|
||||||
@@ -540,7 +593,7 @@ mod tests {
|
|||||||
let two = Card {
|
let two = Card {
|
||||||
suit: Suit::Spade,
|
suit: Suit::Spade,
|
||||||
value: Value::Two,
|
value: Value::Two,
|
||||||
.. Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
klon.waste.push(two.clone());
|
klon.waste.push(two.clone());
|
||||||
let source_card = CardAndPosition {
|
let source_card = CardAndPosition {
|
||||||
@@ -558,12 +611,12 @@ mod tests {
|
|||||||
let ace = Card {
|
let ace = Card {
|
||||||
suit: Suit::Spade,
|
suit: Suit::Spade,
|
||||||
value: Value::Ace,
|
value: Value::Ace,
|
||||||
.. Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let two = Card {
|
let two = Card {
|
||||||
suit: Suit::Spade,
|
suit: Suit::Spade,
|
||||||
value: Value::Two,
|
value: Value::Two,
|
||||||
.. Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
klon.waste.push(two.clone());
|
klon.waste.push(two.clone());
|
||||||
klon.piles[0].push(ace.clone());
|
klon.piles[0].push(ace.clone());
|
||||||
@@ -577,8 +630,6 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert!(klon.move_card_to_foundation(&source_card, &dest_card));
|
assert!(klon.move_card_to_foundation(&source_card, &dest_card));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// TODO the following cases:
|
// TODO the following cases:
|
||||||
// - moving a card from pile to foundation when something is already there
|
// - moving a card from pile to foundation when something is already there
|
||||||
// - moving Ace from waste to top of pile
|
// - moving Ace from waste to top of pile
|
||||||
@@ -592,12 +643,12 @@ mod tests {
|
|||||||
let ace = Card {
|
let ace = Card {
|
||||||
suit: Suit::Heart,
|
suit: Suit::Heart,
|
||||||
value: Value::Ace,
|
value: Value::Ace,
|
||||||
.. Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let two = Card {
|
let two = Card {
|
||||||
suit: Suit::Spade,
|
suit: Suit::Spade,
|
||||||
value: Value::Two,
|
value: Value::Two,
|
||||||
.. Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
klon.waste.push(two.clone());
|
klon.waste.push(two.clone());
|
||||||
klon.piles[0].push(ace.clone());
|
klon.piles[0].push(ace.clone());
|
||||||
@@ -619,12 +670,12 @@ mod tests {
|
|||||||
let ace_heart = Card {
|
let ace_heart = Card {
|
||||||
suit: Suit::Heart,
|
suit: Suit::Heart,
|
||||||
value: Value::Ace,
|
value: Value::Ace,
|
||||||
.. Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let ace_spade = Card {
|
let ace_spade = Card {
|
||||||
suit: Suit::Spade,
|
suit: Suit::Spade,
|
||||||
value: Value::Ace,
|
value: Value::Ace,
|
||||||
.. Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
klon.waste.push(ace_heart.clone());
|
klon.waste.push(ace_heart.clone());
|
||||||
klon.piles[0].push(ace_spade.clone());
|
klon.piles[0].push(ace_spade.clone());
|
||||||
@@ -643,7 +694,7 @@ mod tests {
|
|||||||
fn get_a_whole_deck() {
|
fn get_a_whole_deck() {
|
||||||
let d = Deck::default();
|
let d = Deck::default();
|
||||||
assert_eq!(d.cards.len(), 52); // Probably should test whether all cards are in... eh
|
assert_eq!(d.cards.len(), 52); // Probably should test whether all cards are in... eh
|
||||||
//println!("{:#?}", d); // A "manual" review looks alright
|
//println!("{:#?}", d); // A "manual" review looks alright
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -652,12 +703,12 @@ mod tests {
|
|||||||
let ace = Card {
|
let ace = Card {
|
||||||
suit: Suit::Heart,
|
suit: Suit::Heart,
|
||||||
value: Value::Ace,
|
value: Value::Ace,
|
||||||
.. Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let two = Card {
|
let two = Card {
|
||||||
suit: Suit::Spade,
|
suit: Suit::Spade,
|
||||||
value: Value::Two,
|
value: Value::Two,
|
||||||
.. Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
klon.piles[0].push(two.clone());
|
klon.piles[0].push(two.clone());
|
||||||
klon.piles[1].push(ace.clone());
|
klon.piles[1].push(ace.clone());
|
||||||
@@ -671,18 +722,18 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert!(klon.move_card_to_pile(&source_card, &dest_card));
|
assert!(klon.move_card_to_pile(&source_card, &dest_card));
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn move_pile_card_to_bad_pile() {
|
fn move_pile_card_to_bad_pile() {
|
||||||
let mut klon = Klondike::default();
|
let mut klon = Klondike::default();
|
||||||
let ace = Card {
|
let ace = Card {
|
||||||
suit: Suit::Heart,
|
suit: Suit::Heart,
|
||||||
value: Value::Ace,
|
value: Value::Ace,
|
||||||
.. Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let two = Card {
|
let two = Card {
|
||||||
suit: Suit::Diamond,
|
suit: Suit::Diamond,
|
||||||
value: Value::Two,
|
value: Value::Two,
|
||||||
.. Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
klon.piles[0].push(two.clone());
|
klon.piles[0].push(two.clone());
|
||||||
klon.piles[1].push(ace.clone());
|
klon.piles[1].push(ace.clone());
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1
misc_scripts/mount_truenas_music
Normal file
1
misc_scripts/mount_truenas_music
Normal file
@@ -0,0 +1 @@
|
|||||||
|
sudo mount -t cifs -o uid=arthurr,username=arthurr //truenas.local/Music /home/arthurr/TruenasMusic/
|
||||||
@@ -7,6 +7,14 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4.39", features = ["serde"] }
|
chrono = { version = "0.4.39", features = ["serde"] }
|
||||||
|
clap = { version = "4.5.42", features = ["derive"] }
|
||||||
|
closestmatch = "0.1.2"
|
||||||
|
deunicode = "1.6.2"
|
||||||
|
dir_spec = "0.2.0"
|
||||||
|
rusqlite = "0.37.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0.138"
|
serde_json = "1.0.138"
|
||||||
|
tempfile = "3.20.0"
|
||||||
|
textdistance = "1.1.1"
|
||||||
|
ureq = { version = "3.0.12", features = ["json"] }
|
||||||
uuid = { version = "1.12.1", features = ["v4", "serde"] }
|
uuid = { version = "1.12.1", features = ["v4", "serde"] }
|
||||||
|
|||||||
113
scryfall_deser/README.md
Normal file
113
scryfall_deser/README.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# Quick Magic Finder
|
||||||
|
|
||||||
|
A quick way to search up Magic the Gathering (TM) cards for Linux (maybe MacOS? Don't have one, so haven't tried).
|
||||||
|
|
||||||
|
Currently, no images are displayed - so don't go into this expecting that.
|
||||||
|
|
||||||
|
## The Components
|
||||||
|
|
||||||
|
This repo has 2 main parts to it:
|
||||||
|
|
||||||
|
* The `magic_finder` rust code, which does the "heavy lifting" of updating a database, searching through it for cards, and finding close names (kind of)
|
||||||
|
* The supporting scripts which use [`rofi`](https://github.com/davatorium/rofi)
|
||||||
|
|
||||||
|
`magic_finder` can be used without the rofi parts if you wanted a command for loading showing basic mtg card info from the CLI.
|
||||||
|
|
||||||
|
The `rofi` part is so that I can quickly and easily get the card info I want. Basically just adds a very simple and easy GUI to the `magic_finder` part. I've written 2 wrapper scripts to enable this.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Magic Finder
|
||||||
|
`magic_finder` is written in rust. Check the Cargo.toml file for more specific requirements. I was compiling with rustc version 1.88, but I would not at all be surprised if it worked on rust from year(s) ago.
|
||||||
|
|
||||||
|
### `rofi`
|
||||||
|
The rofi scripts require a version over (I think) 1.7.6. The scripts require the ability to set the `command` setting on the `filebrowser` option in rofi. From what I can tell, this was introduced in 1.7.6.
|
||||||
|
|
||||||
|
Very annoyingly, the Ubuntu repos do *not* have this version. They have an earlier version. So, to use this, you'll need to get a more recent version yourself. I compiled and installed myself. See the end of this README for how I did that.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
**TODO**
|
||||||
|
|
||||||
|
## Installation, First Usage, and Updating
|
||||||
|
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
I am sorry in advance, this is a bit of a pain becuase of my lack of knowledge on how to properly "package" scripts alongside a rust binary.
|
||||||
|
|
||||||
|
**TODO - don't forget that I need to install sqlite3-lib/dev/something - or maybe I add the feature flag "bundled"**
|
||||||
|
|
||||||
|
*TODO - don't forget about adding the shortcut to the overall wrapper script*
|
||||||
|
|
||||||
|
## Before you use this
|
||||||
|
|
||||||
|
## Update
|
||||||
|
Basically the same as the "Before you use this" secion. Go to the [Scryfall Bulk Download](https://scryfall.com/docs/api/bulk-data) page and get the Oracle Cards download.
|
||||||
|
|
||||||
|
Then run the `update_with_rofi.sh` script, locate the downloaded file, and it should Just Work (TM). If not, try updating this repo. If it still doesn't work, log a ticket. It's probably going to something with Scryfall updating their schema that I haven't accounted for. Alternatively, run `COMMAND --update <path to file>` where `<path to file>` is the full path to where you downloaded the file.
|
||||||
|
|
||||||
|
NOTE: Updating *will* delete the previous db - that shouldn't be a problem though, because you shouldn't use that unless you really know what you're doing.
|
||||||
|
|
||||||
|
## Why this exists
|
||||||
|
|
||||||
|
I like watch Magic the Gathering (TM) videos, expecially while coding, working, writing, whatever. Often, I don't know what card they're talking about. They'll often say the card name (sometimes a nickname - this tool doesn't help with that), and show it on the screen briefly (or in a tiny/obscured view), and I'll miss what it actually does. When this happens, I need to open a tab on my browser, go to [Scryfall](scryfall.com), type in the name, (sometimes) click the specific card, and the view it. This takes 2-3 page loads, changing my active window and just a bit of a pain.
|
||||||
|
|
||||||
|
This tool, especially using `rofi` enables me to hit `Ctrl+M`, type in the card name, navigate to the card (if needed) with my keyboard, and display the card. No browser, no HTTP, lower context switch, displayed right there, and goes away when I press anything else.
|
||||||
|
|
||||||
|
The idea is it's just easier and quicker than my normal process.
|
||||||
|
|
||||||
|
## How I Installed `rofi`
|
||||||
|
If you're smarter than me, just follow the official docs: https://github.com/davatorium/rofi/blob/next/INSTALL.md and don't bother reading this.
|
||||||
|
|
||||||
|
I am entirely unfamiliar with `meson` and `ninja` (I'm more a `Makefile` kinda guy - haven't done any `C` properly in >10 years), so here's what I did. This is for Ubutntu - you will need to do something different for download the dependencies (you can see them in the INSTALL.md file referenced above).
|
||||||
|
|
||||||
|
Of note below, I'm installing this into my `$HOME/bin` directory. Change that part if you want to install somewhere else. Make sure to install it somewhere in your `$PATH` though!
|
||||||
|
|
||||||
|
Clone the repo and move into it
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone --recursive https://github.com/davatorium/rofi
|
||||||
|
cd rofi
|
||||||
|
```
|
||||||
|
|
||||||
|
Install the deps
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt build-dep rofi
|
||||||
|
sudo apt install meson
|
||||||
|
sudo apt install libxcb-keysyms1-dev libxcb-keysyms1 # I suspect only one of these is needed - not sure which
|
||||||
|
```
|
||||||
|
|
||||||
|
Setup and build (not sure why I put the prefix in here... you'll see below I still copy+pasted the bin)
|
||||||
|
```
|
||||||
|
meson setup build --prefix $HOME/bin -Dwayland=disabled -Dxcb=enabled
|
||||||
|
ninja -C build -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Fingers crossed that all compiled and stuff... then copy the bin
|
||||||
|
|
||||||
|
```
|
||||||
|
cp build/rofi ~/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
## FIXME, TODO & Features That Could be Good
|
||||||
|
* Remove the non-card cards. Examples I've come across are:
|
||||||
|
** Planes: Black Lotus Lounge
|
||||||
|
** Art Cards: https://scryfall.com/card/altr/15/%C3%A9owyn-fearless-knight-%C3%A9owyn-fearless-knight?utm_source=api
|
||||||
|
* Allow exiting the script early (i.e. I hit CTRL+g just exits everything)
|
||||||
|
* Misspelled cards, if only 1 hit that makes sense, could just work
|
||||||
|
* Display the actual card image (probably won't do this)
|
||||||
|
* Some kind of auto-magic direct link between the return codes set out in `main.r`s and the `rofi` scripts. Currently I need to manually make sure they're the same between the `rust` code and the `sh` code.
|
||||||
|
I'm guessing would involve cargo build scripts (or just a find+replace?)
|
||||||
|
* Add some classic nicknames (might be difficult to find them all). examples include:
|
||||||
|
** Bob - Dark Confidant
|
||||||
|
** AK - Accumulated Knowledge
|
||||||
|
** find more here: https://mtg.wiki/page/List_of_Magic_slang/Card_nicknames
|
||||||
|
|
||||||
|
## Thanks
|
||||||
|
|
||||||
|
This project really is just 95% based on Scryfall. They're amazing. I don't know how or why they exist, but I think they're basically the best Magic the Gathering (TM) resource online.
|
||||||
|
|
||||||
|
Of course `rofi` for providing the quick, simple, low-weight, and well documented tool. Particularly the quickness and low-weight which made it really possible. The alternative was opening terminal windows, or using TKinter or something... surely not worth it.
|
||||||
|
|
||||||
|
Thanks to all the amazing `rust` packages I use.
|
||||||
4
scryfall_deser/scripts/search_with_rofi.sh
Executable file
4
scryfall_deser/scripts/search_with_rofi.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
SEARCH_STRING=$(rofi -l 0 -dmenu)
|
||||||
|
/home/arthurr/code/mini_projects/scryfall_deser/scripts/search_with_rofi_with_args.sh $SEARCH_STRING
|
||||||
74
scryfall_deser/scripts/search_with_rofi_with_args.sh
Executable file
74
scryfall_deser/scripts/search_with_rofi_with_args.sh
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Note to self in this... The whitespace seemed to fuck the ifs up. Not sure why.
|
||||||
|
# This is why it's all flat and ugly
|
||||||
|
|
||||||
|
CARDS=$(/home/arthurr/code/mini_projects/scryfall_deser/target/debug/scryfall_deser $@)
|
||||||
|
RETURN=$?
|
||||||
|
echo $RETURN
|
||||||
|
|
||||||
|
#######################
|
||||||
|
## Exact card found - just print the card
|
||||||
|
#######################
|
||||||
|
if [ $RETURN -eq 200 ]; then
|
||||||
|
|
||||||
|
rofi -e "$CARDS"
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
#######################
|
||||||
|
## Cards to select from
|
||||||
|
#######################
|
||||||
|
if [ $RETURN -eq 0 ]; then
|
||||||
|
|
||||||
|
SELECTION=$(rofi -dmenu -i << EOF
|
||||||
|
$CARDS
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
CARD_OUTPUT=$(/home/arthurr/code/mini_projects/scryfall_deser/target/debug/scryfall_deser --exact $SELECTION)
|
||||||
|
|
||||||
|
# If you double check the first rofi selection it seems to prevent the error window from popping up
|
||||||
|
# I think this is because it registers the second click as a click outside the window which exits
|
||||||
|
# the rofi -e message
|
||||||
|
sleep 0.05
|
||||||
|
|
||||||
|
rofi -e "$CARD_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
##########################
|
||||||
|
## Not even one card that matched - try a close string
|
||||||
|
##########################
|
||||||
|
if [ $RETURN -eq 105 ]; then
|
||||||
|
|
||||||
|
# TODO do something different with no matching string at all - perhaps even a different ExitCode?
|
||||||
|
|
||||||
|
SELECTION=$(rofi -dmenu -p "Did you mean?" -i << EOF
|
||||||
|
$CARDS
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
CARDS=$(/home/arthurr/code/mini_projects/scryfall_deser/target/debug/scryfall_deser $SELECTION)
|
||||||
|
|
||||||
|
SELECTION=$(rofi -dmenu -i << EOF
|
||||||
|
$CARDS
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
CARD_OUTPUT=$(/home/arthurr/code/mini_projects/scryfall_deser/target/debug/scryfall_deser --exact $SELECTION)
|
||||||
|
|
||||||
|
sleep 0.05
|
||||||
|
|
||||||
|
rofi -e "$CARD_OUTPUT"
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
###############################
|
||||||
|
## No seach string input at all
|
||||||
|
###############################
|
||||||
|
if [ $RETURN -eq 101 ]; then
|
||||||
|
|
||||||
|
rofi -e "No search string found"
|
||||||
|
|
||||||
|
fi
|
||||||
9
scryfall_deser/scripts/update_with_rofi.sh
Executable file
9
scryfall_deser/scripts/update_with_rofi.sh
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
SCRYFALL_BULK=$(rofi -modi filebrowser -show filebrowser -filebrowser-command printf)
|
||||||
|
echo "$SCRYFALL_BULK"
|
||||||
|
|
||||||
|
/home/arthurr/code/mini_projects/scryfall_deser/target/debug/scryfall_deser --update "$SCRYFALL_BULK"
|
||||||
|
|
||||||
|
# TODO - check return value
|
||||||
|
rofi -e "Should be updated"
|
||||||
289
scryfall_deser/src/db.rs
Normal file
289
scryfall_deser/src/db.rs
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
use deunicode::deunicode;
|
||||||
|
use rusqlite;
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::fmt;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use super::deser::ScryfallCard;
|
||||||
|
use super::utils::get_local_cache_folder;
|
||||||
|
use super::utils::{create_cache_folder, get_local_data_folder, SQLITE_FILENAME};
|
||||||
|
|
||||||
|
fn get_local_data_sqlite_file() -> PathBuf {
|
||||||
|
let mut folder = get_local_data_folder();
|
||||||
|
folder.push(SQLITE_FILENAME);
|
||||||
|
folder
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_card_names() -> Vec<String> {
|
||||||
|
let sqlite_file = get_local_data_sqlite_file();
|
||||||
|
let conn = rusqlite::Connection::open(sqlite_file).unwrap();
|
||||||
|
let mut stmt = conn.prepare("SELECT name FROM cards;").unwrap();
|
||||||
|
let mut rows = stmt.query([]).unwrap();
|
||||||
|
let mut card_names = Vec::new();
|
||||||
|
while let Some(row) = rows.next().unwrap() {
|
||||||
|
card_names.push(row.get(0).unwrap());
|
||||||
|
}
|
||||||
|
card_names
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_lowercase_card_names() -> Vec<String> {
|
||||||
|
let sqlite_file = get_local_data_sqlite_file();
|
||||||
|
let conn = rusqlite::Connection::open(sqlite_file).unwrap();
|
||||||
|
let mut stmt = conn.prepare("SELECT lowercase_name FROM cards;").unwrap();
|
||||||
|
let mut rows = stmt.query([]).unwrap();
|
||||||
|
let mut card_names = Vec::new();
|
||||||
|
while let Some(row) = rows.next().unwrap() {
|
||||||
|
card_names.push(row.get(0).unwrap());
|
||||||
|
}
|
||||||
|
card_names
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_mtg_words() -> Vec<String> {
|
||||||
|
let sqlite_file = get_local_data_sqlite_file();
|
||||||
|
let conn = rusqlite::Connection::open(sqlite_file).unwrap();
|
||||||
|
let mut stmt = conn.prepare("SELECT word FROM mtg_words;").unwrap();
|
||||||
|
let mut rows = stmt.query([]).unwrap();
|
||||||
|
let mut card_names = Vec::new();
|
||||||
|
while let Some(row) = rows.next().unwrap() {
|
||||||
|
card_names.push(row.get(0).unwrap());
|
||||||
|
}
|
||||||
|
card_names
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsure if this should be in this file...
|
||||||
|
impl fmt::Display for DbCard {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match &self.mana_cost {
|
||||||
|
Some(mc) => writeln!(f, "{}\t{}", self.name, mc)?,
|
||||||
|
None => writeln!(f, "{}", self.name)?,
|
||||||
|
}
|
||||||
|
writeln!(f, "{}", self.type_line)?;
|
||||||
|
match &self.oracle_text {
|
||||||
|
Some(ot) => writeln!(f, "{}", ot)?,
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
match &self.power_toughness {
|
||||||
|
Some(pt) => writeln!(f, "{}", pt)?,
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
match &self.loyalty {
|
||||||
|
Some(l) => writeln!(f, "Starting Loyalty: {}", l)?,
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
writeln!(f, "Scryfall URI: {}", self.scryfall_uri)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for DbCard {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.name.cmp(&other.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for DbCard {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for DbCard {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.name == other.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for DbCard {}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DbCard {
|
||||||
|
pub name: String,
|
||||||
|
pub lowercase_name: String,
|
||||||
|
pub type_line: String,
|
||||||
|
pub oracle_text: Option<String>,
|
||||||
|
pub power_toughness: Option<String>,
|
||||||
|
pub loyalty: Option<String>,
|
||||||
|
pub mana_cost: Option<String>,
|
||||||
|
pub scryfall_uri: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum GetNameType {
|
||||||
|
Name,
|
||||||
|
LowercaseName,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_card_by_name(name: &str, name_type: GetNameType) -> Option<DbCard> {
|
||||||
|
let sqlite_file = get_local_data_sqlite_file();
|
||||||
|
let conn = rusqlite::Connection::open(sqlite_file).unwrap();
|
||||||
|
let sql = match name_type {
|
||||||
|
GetNameType::Name => {
|
||||||
|
"SELECT name, lowercase_name, type_line, oracle_text, power_toughness, loyalty, mana_cost, scryfall_uri
|
||||||
|
FROM cards WHERE name = (?1)"
|
||||||
|
}
|
||||||
|
GetNameType::LowercaseName => {
|
||||||
|
"SELECT name, lowercase_name, type_line, oracle_text, power_toughness, loyalty, mana_cost, scryfall_uri
|
||||||
|
FROM cards WHERE lowercase_name = (?1)"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
dbg!(name);
|
||||||
|
dbg!(&sql);
|
||||||
|
let mut stmt = conn.prepare(sql).unwrap();
|
||||||
|
let mut rows = stmt.query([name]).unwrap();
|
||||||
|
match rows.next().unwrap() {
|
||||||
|
Some(row) => Some(DbCard {
|
||||||
|
name: row.get(0).unwrap(),
|
||||||
|
lowercase_name: row.get(1).unwrap(),
|
||||||
|
type_line: row.get(2).unwrap(),
|
||||||
|
oracle_text: row.get(3).unwrap(),
|
||||||
|
power_toughness: row.get(4).unwrap(),
|
||||||
|
loyalty: row.get(5).unwrap(),
|
||||||
|
mana_cost: row.get(6).unwrap(),
|
||||||
|
scryfall_uri: row.get(7).unwrap(),
|
||||||
|
}),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_matching_cards_scryfall_style(search_strings: &Vec<String>) -> Vec<DbCard> {
|
||||||
|
assert!(!search_strings.is_empty());
|
||||||
|
let sqlite_file = get_local_data_sqlite_file();
|
||||||
|
let conn = rusqlite::Connection::open(sqlite_file).unwrap();
|
||||||
|
let mut percentaged_string = Vec::new();
|
||||||
|
// I know that .clone fixes my problem - I'm not sure why I need to though
|
||||||
|
for mut search_string in search_strings.clone() {
|
||||||
|
search_string.push('%');
|
||||||
|
search_string.insert(0, '%');
|
||||||
|
percentaged_string.push(search_string);
|
||||||
|
}
|
||||||
|
let mut sql: String = "SELECT name, lowercase_name, type_line, oracle_text, power_toughness, loyalty, mana_cost, scryfall_uri
|
||||||
|
FROM cards WHERE".into();
|
||||||
|
for i in 0..search_strings.len() {
|
||||||
|
sql.push_str(&format!(" lowercase_name LIKE (?{}) AND", i + 1));
|
||||||
|
}
|
||||||
|
sql.pop();
|
||||||
|
sql.pop();
|
||||||
|
sql.pop();
|
||||||
|
sql.pop();
|
||||||
|
let mut stmt = conn.prepare(&sql).unwrap();
|
||||||
|
stmt.query_map(rusqlite::params_from_iter(percentaged_string), |row| {
|
||||||
|
Ok(DbCard {
|
||||||
|
name: row.get(0).unwrap(),
|
||||||
|
lowercase_name: row.get(1).unwrap(),
|
||||||
|
type_line: row.get(2).unwrap(),
|
||||||
|
oracle_text: row.get(3).unwrap(),
|
||||||
|
power_toughness: row.get(4).unwrap(),
|
||||||
|
loyalty: row.get(5).unwrap(),
|
||||||
|
mana_cost: row.get(6).unwrap(),
|
||||||
|
scryfall_uri: row.get(7).unwrap(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.filter_map(|res| res.ok())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_matching_cards(name: &str) -> Vec<DbCard> {
|
||||||
|
let sqlite_file = get_local_data_sqlite_file();
|
||||||
|
let conn = rusqlite::Connection::open(sqlite_file).unwrap();
|
||||||
|
// There must be something better than this - although I don't think it's possible with a str
|
||||||
|
let mut name = name.to_string();
|
||||||
|
name.push('%');
|
||||||
|
name.insert(0, '%');
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare(
|
||||||
|
"SELECT name, lowercase_name, type_line, oracle_text, power_toughness, loyalty, mana_cost, scryfall_uri
|
||||||
|
FROM cards WHERE lowercase_name LIKE (?1)",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
stmt.query_map([name], |row| {
|
||||||
|
Ok(DbCard {
|
||||||
|
name: row.get(0).unwrap(),
|
||||||
|
lowercase_name: row.get(1).unwrap(),
|
||||||
|
type_line: row.get(2).unwrap(),
|
||||||
|
oracle_text: row.get(3).unwrap(),
|
||||||
|
power_toughness: row.get(4).unwrap(),
|
||||||
|
loyalty: row.get(5).unwrap(),
|
||||||
|
mana_cost: row.get(6).unwrap(),
|
||||||
|
scryfall_uri: row.get(7).unwrap(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.filter_map(|res| res.ok())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
const CREATE_CARDS_TABLE_SQL: &str = "
|
||||||
|
CREATE TABLE cards (
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
lowercase_name TEXT NOT NULL UNIQUE,
|
||||||
|
type_line TEXT,
|
||||||
|
oracle_text TEXT,
|
||||||
|
power_toughness TEXT,
|
||||||
|
loyalty TEXT,
|
||||||
|
mana_cost TEXT,
|
||||||
|
scryfall_uri TEXT NOT NULL UNIQUE
|
||||||
|
)";
|
||||||
|
|
||||||
|
const CREATE_MAGIC_WORDS_TABLE_SQL: &str = "
|
||||||
|
CREATE TABLE mtg_words (
|
||||||
|
word TEXT NOT NULL UNIQUE
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Will delete your current db
|
||||||
|
pub fn init_db() -> bool {
|
||||||
|
create_cache_folder();
|
||||||
|
let sqlite_file = get_local_data_sqlite_file();
|
||||||
|
println!("sqlite file location: {}", sqlite_file.display());
|
||||||
|
// TESTING
|
||||||
|
println!("cache folder: {}", get_local_cache_folder().display());
|
||||||
|
let _res = fs::remove_file(&sqlite_file);
|
||||||
|
// TODO actually check result for whether it was a permissions thing or something
|
||||||
|
let connection = rusqlite::Connection::open(sqlite_file).unwrap();
|
||||||
|
connection.execute(&CREATE_CARDS_TABLE_SQL, ()).unwrap();
|
||||||
|
connection
|
||||||
|
.execute(&CREATE_MAGIC_WORDS_TABLE_SQL, ())
|
||||||
|
.unwrap();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_db_with_file(file: PathBuf) -> bool {
|
||||||
|
let ac = fs::read_to_string(file).unwrap();
|
||||||
|
let ac: Vec<ScryfallCard> = serde_json::from_str(&ac).unwrap();
|
||||||
|
let sqlite_file = get_local_data_sqlite_file();
|
||||||
|
let mut conn = rusqlite::Connection::open(sqlite_file).unwrap();
|
||||||
|
let tx = conn.transaction().unwrap();
|
||||||
|
for card in ac {
|
||||||
|
for word in card.name.split_whitespace() {
|
||||||
|
let word = deunicode(&word.to_lowercase());
|
||||||
|
let res = tx.execute(
|
||||||
|
"INSERT INTO mtg_words (word) VALUES (?1)
|
||||||
|
ON CONFLICT (word) DO NOTHING;",
|
||||||
|
[word.replace(",", "")],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let lowercase_name = deunicode(&card.name.to_lowercase());
|
||||||
|
let power_toughness = match card.power {
|
||||||
|
Some(p) => Some(format!("{}/{}", p, card.toughness.unwrap())),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let oracle_text = match card.oracle_text {
|
||||||
|
Some(ot) => ot,
|
||||||
|
None => "<No Oracle Text>".to_string(),
|
||||||
|
};
|
||||||
|
let loyalty = match card.loyalty {
|
||||||
|
Some(loy) => Some(loy),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let mana_cost = match card.mana_cost {
|
||||||
|
Some(mc) => Some(mc),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let res = tx.execute(
|
||||||
|
"INSERT INTO cards (name, lowercase_name, type_line, oracle_text, power_toughness, loyalty, mana_cost, scryfall_uri) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||||
|
rusqlite::params![card.name, lowercase_name, card.type_line, oracle_text, power_toughness, loyalty, mana_cost, card.scryfall_uri],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
tx.commit();
|
||||||
|
true
|
||||||
|
}
|
||||||
788
scryfall_deser/src/deser.rs
Normal file
788
scryfall_deser/src/deser.rs
Normal file
@@ -0,0 +1,788 @@
|
|||||||
|
use chrono::NaiveDate;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::Value;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
// Info from here:
|
||||||
|
// https://scryfall.com/docs/api/cards
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct ScryfallCard {
|
||||||
|
// Core Card Fields
|
||||||
|
pub arena_id: Option<u64>,
|
||||||
|
pub id: Uuid,
|
||||||
|
pub lang: String,
|
||||||
|
pub mtgo_id: Option<u64>,
|
||||||
|
pub mtgo_foil_id: Option<u64>,
|
||||||
|
pub multiverse_ids: Option<Vec<u64>>,
|
||||||
|
pub tcgplayer_id: Option<u64>,
|
||||||
|
pub tcgplayer_etched_id: Option<u64>,
|
||||||
|
pub cardmarket_id: Option<u64>,
|
||||||
|
pub object: String,
|
||||||
|
pub layout: String, // Perhaps some kind of enum of these: https://scryfall.com/docs/api/layouts?
|
||||||
|
pub oracle_id: Option<Uuid>,
|
||||||
|
pub prints_search_uri: String, // URI
|
||||||
|
pub rulings_uri: String, // URI
|
||||||
|
pub scryfall_uri: String, // URI
|
||||||
|
pub uri: String, // URI
|
||||||
|
|
||||||
|
// Gameplay Fields
|
||||||
|
// https://scryfall.com/docs/api/cards#gameplay-fields
|
||||||
|
pub all_parts: Option<Vec<ScryfallRelatedCardObject>>,
|
||||||
|
pub card_faces: Option<Vec<ScryfallCardFaceObject>>,
|
||||||
|
|
||||||
|
// NOTE: Much of the next is a repeat of what's in the ScryfallCardFaceObject if you change something here, change something there
|
||||||
|
// NOTE: Probably a bad idea to rename color -> colour just for the sake
|
||||||
|
pub cmc: Option<f64>, // TODO: Make this a proper Decimal - see "Little Girl" card for example of cmc of 0.5
|
||||||
|
#[serde(rename = "color_identity")]
|
||||||
|
pub colour_identity: Option<Vec<Colour>>,
|
||||||
|
#[serde(rename = "color_indicator")]
|
||||||
|
pub colour_indicator: Option<Vec<Colour>>,
|
||||||
|
#[serde(rename = "colors")]
|
||||||
|
pub colours: Option<Vec<Colour>>,
|
||||||
|
pub edhrec_rank: Option<u64>,
|
||||||
|
pub defense: Option<String>,
|
||||||
|
pub game_changer: bool,
|
||||||
|
pub hand_modifier: Option<String>,
|
||||||
|
pub keywords: Vec<String>, // Words like "Flying"
|
||||||
|
pub legalities: FormatLegalities,
|
||||||
|
pub life_modifier: Option<String>,
|
||||||
|
pub loyalty: Option<String>,
|
||||||
|
pub mana_cost: Option<String>,
|
||||||
|
pub name: String,
|
||||||
|
pub oracle_text: Option<String>,
|
||||||
|
pub penny_rank: Option<u64>,
|
||||||
|
pub power: Option<String>,
|
||||||
|
pub produced_mana: Option<Vec<Colour>>,
|
||||||
|
pub reserved: bool,
|
||||||
|
pub toughness: Option<String>,
|
||||||
|
pub type_line: String,
|
||||||
|
|
||||||
|
// Print Fields
|
||||||
|
// https://scryfall.com/docs/api/cards#print-fields
|
||||||
|
pub artist: Option<String>,
|
||||||
|
pub artist_ids: Option<Vec<String>>,
|
||||||
|
pub attraction_lights: Option<Vec<u8>>,
|
||||||
|
pub booster: bool,
|
||||||
|
#[serde(rename = "border_color")]
|
||||||
|
pub border_colour: BorderColour,
|
||||||
|
pub card_back_id: Option<Uuid>, // Scryfall docs says this should not be null, but ZHS Growing Rites of Itlimoc seems to not have one... maybe it's the back side?
|
||||||
|
pub collector_number: String,
|
||||||
|
pub content_warning: Option<bool>,
|
||||||
|
pub digital: bool,
|
||||||
|
pub finishes: Vec<Finish>,
|
||||||
|
#[serde(rename = "flavor_name")]
|
||||||
|
pub flavour_name: Option<String>,
|
||||||
|
#[serde(rename = "flavor_text")]
|
||||||
|
pub flavour_text: Option<String>,
|
||||||
|
pub frame_effects: Option<Vec<FrameEffect>>,
|
||||||
|
pub frame: Frame,
|
||||||
|
pub full_art: bool,
|
||||||
|
pub games: Vec<Game>,
|
||||||
|
pub highres_image: bool,
|
||||||
|
pub illustration_id: Option<Uuid>,
|
||||||
|
pub image_status: ImageStatus,
|
||||||
|
pub image_uris: Option<ImageURIs>,
|
||||||
|
pub oversized: bool,
|
||||||
|
pub prices: Prices,
|
||||||
|
pub printed_name: Option<String>,
|
||||||
|
pub printed_text: Option<String>,
|
||||||
|
pub printed_type_line: Option<String>,
|
||||||
|
pub promo: bool,
|
||||||
|
//pub promo_types: Option<Vec<PromoTypes>>,
|
||||||
|
pub promo_types: Option<Vec<String>>,
|
||||||
|
pub purchase_uris: Option<PurchaseUris>,
|
||||||
|
pub rarity: Rarity,
|
||||||
|
pub related_uris: Value, // TODO: - list all the URIs? Maybe? Who cares?
|
||||||
|
pub released_at: NaiveDate,
|
||||||
|
pub reprint: bool,
|
||||||
|
pub scryfall_set_uri: String, // URI
|
||||||
|
pub set_name: String,
|
||||||
|
pub set_search_uri: String, // URI
|
||||||
|
pub set_type: SetType,
|
||||||
|
pub set_uri: String, // URI
|
||||||
|
pub set: String,
|
||||||
|
pub set_id: Uuid,
|
||||||
|
pub story_spotlight: bool,
|
||||||
|
pub textless: bool,
|
||||||
|
pub variation: bool,
|
||||||
|
pub variation_of: Option<Uuid>,
|
||||||
|
pub security_stamp: Option<SecurityStamp>,
|
||||||
|
pub watermark: Option<String>,
|
||||||
|
pub preview: Option<Preview>,
|
||||||
|
|
||||||
|
// These aren't in the Scryfall docs, but some cards do have 'em
|
||||||
|
pub foil: Option<bool>,
|
||||||
|
pub nonfoil: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://scryfall.com/docs/api/cards#card-face-objects
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct ScryfallCardFaceObject {
|
||||||
|
pub artist: Option<String>,
|
||||||
|
pub artist_id: Option<Uuid>, // UUID
|
||||||
|
pub cmc: Option<f64>, // TODO: Make this a proper Decimal - see "Little Girl" card for example of cmc of 0.5
|
||||||
|
#[serde(rename = "color_identity")]
|
||||||
|
pub colour_identity: Option<Vec<Colour>>,
|
||||||
|
#[serde(rename = "color_indicator")]
|
||||||
|
pub colour_indicator: Option<Vec<Colour>>,
|
||||||
|
#[serde(rename = "colors")]
|
||||||
|
pub colours: Option<Vec<Colour>>,
|
||||||
|
pub defense: Option<String>,
|
||||||
|
pub flavour_text: Option<String>,
|
||||||
|
pub illustration_id: Option<Uuid>,
|
||||||
|
pub image_uris: Option<ImageURIs>,
|
||||||
|
pub layout: Option<String>,
|
||||||
|
pub loyalty: Option<String>,
|
||||||
|
pub mana_cost: Option<String>,
|
||||||
|
pub name: String,
|
||||||
|
pub object: String,
|
||||||
|
pub oracle_id: Option<Uuid>,
|
||||||
|
pub oracle_text: Option<String>,
|
||||||
|
pub power: Option<String>,
|
||||||
|
pub printed_name: Option<String>,
|
||||||
|
pub printed_text: Option<String>,
|
||||||
|
pub printed_type_line: Option<String>,
|
||||||
|
pub toughness: Option<String>,
|
||||||
|
pub type_line: Option<String>,
|
||||||
|
pub watermark: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://scryfall.com/docs/api/cards#related-card-objects
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct ScryfallRelatedCardObject {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub object: String, // Always "related_card"
|
||||||
|
pub component: Component,
|
||||||
|
pub name: String,
|
||||||
|
pub type_line: String,
|
||||||
|
pub uri: String, // URI
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, PartialEq, Debug)]
|
||||||
|
pub enum Colour {
|
||||||
|
#[serde(rename = "W")]
|
||||||
|
White,
|
||||||
|
#[serde(rename = "U")]
|
||||||
|
Blue,
|
||||||
|
#[serde(rename = "B")]
|
||||||
|
Black,
|
||||||
|
#[serde(rename = "R")]
|
||||||
|
Red,
|
||||||
|
#[serde(rename = "G")]
|
||||||
|
Green,
|
||||||
|
#[serde(rename = "C")] // I don't think it's meant to work like this... but eh
|
||||||
|
Colourless,
|
||||||
|
#[serde(rename = "T")] // See "Sole Performer"
|
||||||
|
Tap,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub enum Legality {
|
||||||
|
#[serde(rename = "legal")]
|
||||||
|
Legal,
|
||||||
|
#[serde(rename = "not_legal")]
|
||||||
|
NotLegal,
|
||||||
|
#[serde(rename = "banned")]
|
||||||
|
Banned,
|
||||||
|
#[serde(rename = "restricted")]
|
||||||
|
Restricted,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct FormatLegalities {
|
||||||
|
standard: Legality,
|
||||||
|
future: Legality,
|
||||||
|
historic: Legality,
|
||||||
|
timeless: Legality,
|
||||||
|
gladiator: Legality,
|
||||||
|
pioneer: Legality,
|
||||||
|
modern: Legality,
|
||||||
|
legacy: Legality,
|
||||||
|
pauper: Legality,
|
||||||
|
vintage: Legality,
|
||||||
|
penny: Legality,
|
||||||
|
commander: Legality,
|
||||||
|
oathbreaker: Legality,
|
||||||
|
standardbrawl: Legality,
|
||||||
|
brawl: Legality,
|
||||||
|
alchemy: Legality,
|
||||||
|
paupercommander: Legality,
|
||||||
|
duel: Legality,
|
||||||
|
oldschool: Legality,
|
||||||
|
premodern: Legality,
|
||||||
|
predh: Legality,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub enum BorderColour {
|
||||||
|
#[serde(rename = "black")]
|
||||||
|
Black,
|
||||||
|
#[serde(rename = "white")]
|
||||||
|
White,
|
||||||
|
#[serde(rename = "borderless")]
|
||||||
|
Borderless,
|
||||||
|
#[serde(rename = "yellow")]
|
||||||
|
Yellow,
|
||||||
|
#[serde(rename = "silver")]
|
||||||
|
Silver,
|
||||||
|
#[serde(rename = "gold")]
|
||||||
|
Gold,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub enum Finish {
|
||||||
|
#[serde(rename = "foil")]
|
||||||
|
Foil,
|
||||||
|
#[serde(rename = "nonfoil")]
|
||||||
|
NonFoil,
|
||||||
|
#[serde(rename = "etched")]
|
||||||
|
Etched,
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://scryfall.com/docs/api/frames#frames
|
||||||
|
// This is probably dumb...
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub enum Frame {
|
||||||
|
#[serde(rename = "1993")]
|
||||||
|
NinetyThree,
|
||||||
|
#[serde(rename = "1997")]
|
||||||
|
NinetySeven,
|
||||||
|
#[serde(rename = "2003")]
|
||||||
|
OhThree,
|
||||||
|
#[serde(rename = "2015")]
|
||||||
|
Fifteen,
|
||||||
|
#[serde(rename = "future")]
|
||||||
|
Future,
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://scryfall.com/docs/api/frames#frame-effects
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub enum FrameEffect {
|
||||||
|
#[serde(rename = "legendary")]
|
||||||
|
Legendary,
|
||||||
|
#[serde(rename = "miracle")]
|
||||||
|
Miracle,
|
||||||
|
#[serde(rename = "enchantment")]
|
||||||
|
Enchantment,
|
||||||
|
#[serde(rename = "draft")]
|
||||||
|
Draft,
|
||||||
|
#[serde(rename = "devoid")]
|
||||||
|
Devoid,
|
||||||
|
#[serde(rename = "tombstone")]
|
||||||
|
Tombstone,
|
||||||
|
#[serde(rename = "colorshifted")]
|
||||||
|
Colourshifted,
|
||||||
|
#[serde(rename = "inverted")]
|
||||||
|
Inverted,
|
||||||
|
#[serde(rename = "sunmoondfc")]
|
||||||
|
SunMoonDFC,
|
||||||
|
#[serde(rename = "compasslanddfc")]
|
||||||
|
CompassLandDFC,
|
||||||
|
#[serde(rename = "originpwdfc")]
|
||||||
|
OriginPwDFC,
|
||||||
|
#[serde(rename = "mooneldrazidfc")]
|
||||||
|
MoonEldraziDFC,
|
||||||
|
#[serde(rename = "waxingandwaningmoondfc")]
|
||||||
|
WaxingAndWaningMoonDFC,
|
||||||
|
#[serde(rename = "showcase")]
|
||||||
|
Showcase,
|
||||||
|
#[serde(rename = "extendedart")]
|
||||||
|
ExtendedArt,
|
||||||
|
#[serde(rename = "companion")]
|
||||||
|
Companion,
|
||||||
|
#[serde(rename = "etched")]
|
||||||
|
Etched,
|
||||||
|
#[serde(rename = "snow")]
|
||||||
|
Snow,
|
||||||
|
#[serde(rename = "lesson")]
|
||||||
|
Lesson,
|
||||||
|
#[serde(rename = "shatteredglass")]
|
||||||
|
ShatteredGlass,
|
||||||
|
#[serde(rename = "convertdfc")]
|
||||||
|
ConvertDFC,
|
||||||
|
#[serde(rename = "fandfc")]
|
||||||
|
FanDFC,
|
||||||
|
#[serde(rename = "upsidedowndfc")]
|
||||||
|
UpsideDownDFC,
|
||||||
|
#[serde(rename = "spree")]
|
||||||
|
Spree,
|
||||||
|
#[serde(rename = "fullart")]
|
||||||
|
FullArt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub enum Game {
|
||||||
|
#[serde(rename = "paper")]
|
||||||
|
Paper,
|
||||||
|
#[serde(rename = "mtgo")]
|
||||||
|
Mtgo,
|
||||||
|
#[serde(rename = "arena")]
|
||||||
|
Arena,
|
||||||
|
#[serde(rename = "astral")]
|
||||||
|
Astral,
|
||||||
|
#[serde(rename = "sega")]
|
||||||
|
Sega,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub enum ImageStatus {
|
||||||
|
#[serde(rename = "missing")]
|
||||||
|
Missing,
|
||||||
|
#[serde(rename = "placeholder")]
|
||||||
|
Placeholder,
|
||||||
|
#[serde(rename = "lowres")]
|
||||||
|
LowResolution,
|
||||||
|
#[serde(rename = "highres_scan")]
|
||||||
|
HighResolutionScan,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct ImageURIs {
|
||||||
|
png: Option<String>,
|
||||||
|
border_crop: Option<String>,
|
||||||
|
art_crop: Option<String>,
|
||||||
|
large: Option<String>,
|
||||||
|
normal: Option<String>,
|
||||||
|
small: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct Preview {
|
||||||
|
pub previewed_at: Option<NaiveDate>,
|
||||||
|
pub source_uri: Option<String>, // URI
|
||||||
|
pub source: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct Prices {
|
||||||
|
usd: Option<String>, // TODO Convert to f64?
|
||||||
|
usd_foil: Option<String>,
|
||||||
|
usd_etched: Option<String>,
|
||||||
|
eur: Option<String>,
|
||||||
|
eur_foil: Option<String>,
|
||||||
|
tix: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub enum Rarity {
|
||||||
|
#[serde(rename = "common")]
|
||||||
|
Common,
|
||||||
|
#[serde(rename = "uncommon")]
|
||||||
|
Uncommon,
|
||||||
|
#[serde(rename = "rare")]
|
||||||
|
Rare,
|
||||||
|
#[serde(rename = "special")]
|
||||||
|
Special,
|
||||||
|
#[serde(rename = "mythic")]
|
||||||
|
Mythic,
|
||||||
|
#[serde(rename = "bonus")]
|
||||||
|
Bonus,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct PurchaseUris {
|
||||||
|
tcgplayer: String, // Option?
|
||||||
|
cardmarket: String,
|
||||||
|
cardhoarder: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub enum SecurityStamp {
|
||||||
|
#[serde(rename = "oval")]
|
||||||
|
Oval,
|
||||||
|
#[serde(rename = "triangle")]
|
||||||
|
Triangle,
|
||||||
|
#[serde(rename = "acorn")]
|
||||||
|
Acorn,
|
||||||
|
#[serde(rename = "circle")]
|
||||||
|
Circle,
|
||||||
|
#[serde(rename = "arena")]
|
||||||
|
Arena,
|
||||||
|
#[serde(rename = "heart")]
|
||||||
|
Heart,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub enum Component {
|
||||||
|
#[serde(rename = "token")]
|
||||||
|
Token,
|
||||||
|
#[serde(rename = "meld_part")]
|
||||||
|
MeldPart,
|
||||||
|
#[serde(rename = "meld_result")]
|
||||||
|
MeldResult,
|
||||||
|
#[serde(rename = "combo_piece")]
|
||||||
|
ComboPiece,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub enum SetType {
|
||||||
|
#[serde(rename = "alchemy")]
|
||||||
|
Alchemy,
|
||||||
|
#[serde(rename = "archenemy")]
|
||||||
|
Archenemy,
|
||||||
|
#[serde(rename = "arsenal")]
|
||||||
|
Arsenal,
|
||||||
|
#[serde(rename = "box")]
|
||||||
|
Box,
|
||||||
|
#[serde(rename = "commander")]
|
||||||
|
Commander,
|
||||||
|
#[serde(rename = "core")]
|
||||||
|
Core,
|
||||||
|
#[serde(rename = "draft_innovation")]
|
||||||
|
DraftInnovation,
|
||||||
|
#[serde(rename = "duel_deck")]
|
||||||
|
DuelDeck,
|
||||||
|
#[serde(rename = "expansion")]
|
||||||
|
Expansion,
|
||||||
|
#[serde(rename = "from_the_vault")]
|
||||||
|
FromTheVault,
|
||||||
|
#[serde(rename = "funny")]
|
||||||
|
Funny,
|
||||||
|
#[serde(rename = "masterpiece")]
|
||||||
|
Masterpiece,
|
||||||
|
#[serde(rename = "masters")]
|
||||||
|
Masters,
|
||||||
|
#[serde(rename = "memorabilia")]
|
||||||
|
Memorabilia,
|
||||||
|
#[serde(rename = "minigame")]
|
||||||
|
Minigame,
|
||||||
|
#[serde(rename = "planechase")]
|
||||||
|
Planechase,
|
||||||
|
#[serde(rename = "premium_deck")]
|
||||||
|
PremiumDeck,
|
||||||
|
#[serde(rename = "promo")]
|
||||||
|
Promo,
|
||||||
|
#[serde(rename = "spellbook")]
|
||||||
|
SpellBook,
|
||||||
|
#[serde(rename = "starter")]
|
||||||
|
Starter,
|
||||||
|
#[serde(rename = "token")]
|
||||||
|
Token,
|
||||||
|
#[serde(rename = "treasure_chest")]
|
||||||
|
TreasureChest,
|
||||||
|
#[serde(rename = "vanguard")]
|
||||||
|
Vanguard,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Complete this
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub enum PromoTypes {
|
||||||
|
#[serde(rename = "alchemy")]
|
||||||
|
Alchemy,
|
||||||
|
#[serde(rename = "arenaleague")]
|
||||||
|
ArenaLeague,
|
||||||
|
#[serde(rename = "beginnerbox")]
|
||||||
|
BeginnerBox,
|
||||||
|
#[serde(rename = "boosterfun")]
|
||||||
|
BoosterFun,
|
||||||
|
#[serde(rename = "boxtopper")]
|
||||||
|
BoxTopper,
|
||||||
|
#[serde(rename = "brawldeck")]
|
||||||
|
BrawlDeck,
|
||||||
|
#[serde(rename = "bundle")]
|
||||||
|
Bundle,
|
||||||
|
#[serde(rename = "buyabox")]
|
||||||
|
BuyABox,
|
||||||
|
#[serde(rename = "confettifoil")]
|
||||||
|
ConfettiFoil,
|
||||||
|
#[serde(rename = "convention")]
|
||||||
|
Convention,
|
||||||
|
#[serde(rename = "datestamped")]
|
||||||
|
DateStamped,
|
||||||
|
#[serde(rename = "dossier")]
|
||||||
|
Dossier,
|
||||||
|
#[serde(rename = "doublerainbow")]
|
||||||
|
DoubleRainbow,
|
||||||
|
#[serde(rename = "embossed")]
|
||||||
|
Embossed,
|
||||||
|
#[serde(rename = "event")]
|
||||||
|
Event,
|
||||||
|
#[serde(rename = "fnm")]
|
||||||
|
Fnm,
|
||||||
|
#[serde(rename = "gameday")]
|
||||||
|
GameDay,
|
||||||
|
#[serde(rename = "godzillaseries")]
|
||||||
|
GodzillaSeries,
|
||||||
|
#[serde(rename = "halofoil")]
|
||||||
|
HaloFoil,
|
||||||
|
#[serde(rename = "imagine")]
|
||||||
|
Imagine,
|
||||||
|
#[serde(rename = "instore")]
|
||||||
|
InStore,
|
||||||
|
#[serde(rename = "intropack")]
|
||||||
|
IntroPack,
|
||||||
|
#[serde(rename = "invisibleink")]
|
||||||
|
InvisibleInk,
|
||||||
|
#[serde(rename = "judgegift")]
|
||||||
|
JudgeGift,
|
||||||
|
#[serde(rename = "league")]
|
||||||
|
League,
|
||||||
|
#[serde(rename = "magnified")]
|
||||||
|
Magnified,
|
||||||
|
#[serde(rename = "manafoil")]
|
||||||
|
ManaFoil,
|
||||||
|
#[serde(rename = "mediainsert")]
|
||||||
|
MediaInsert,
|
||||||
|
#[serde(rename = "planeswalkerdeck")]
|
||||||
|
PlaneswalkerDeck,
|
||||||
|
#[serde(rename = "plastic")]
|
||||||
|
Plastic,
|
||||||
|
#[serde(rename = "playerrewards")]
|
||||||
|
PlayerRewards,
|
||||||
|
#[serde(rename = "playtest")]
|
||||||
|
Playtest,
|
||||||
|
#[serde(rename = "poster")]
|
||||||
|
Poster,
|
||||||
|
#[serde(rename = "prerelease")]
|
||||||
|
Prerelease,
|
||||||
|
#[serde(rename = "premiereshop")]
|
||||||
|
PremiereShop,
|
||||||
|
#[serde(rename = "promopack")]
|
||||||
|
PromoPack,
|
||||||
|
#[serde(rename = "rainbowfoil")]
|
||||||
|
RainbowFoil,
|
||||||
|
#[serde(rename = "ravnicacity")]
|
||||||
|
RavnicaCity,
|
||||||
|
#[serde(rename = "rebalanced")]
|
||||||
|
Rebalanced,
|
||||||
|
#[serde(rename = "release")]
|
||||||
|
Release,
|
||||||
|
#[serde(rename = "resale")]
|
||||||
|
Resale,
|
||||||
|
#[serde(rename = "ripplefoil")]
|
||||||
|
RippleFoil,
|
||||||
|
#[serde(rename = "setpromo")]
|
||||||
|
SetPromo,
|
||||||
|
#[serde(rename = "serialized")]
|
||||||
|
Serialised,
|
||||||
|
#[serde(rename = "silverfoil")]
|
||||||
|
SilverFoil,
|
||||||
|
#[serde(rename = "sldbonus")]
|
||||||
|
SldBonus,
|
||||||
|
#[serde(rename = "stamped")]
|
||||||
|
Stamped,
|
||||||
|
#[serde(rename = "startercollection")]
|
||||||
|
StarterCollection,
|
||||||
|
#[serde(rename = "starterdeck")]
|
||||||
|
StarterDeck,
|
||||||
|
#[serde(rename = "stepandcompleat")]
|
||||||
|
StepAndCompleat,
|
||||||
|
#[serde(rename = "surgefoil")]
|
||||||
|
SurgeFoil,
|
||||||
|
#[serde(rename = "textured")]
|
||||||
|
Textured,
|
||||||
|
#[serde(rename = "themepack")]
|
||||||
|
ThemePack,
|
||||||
|
#[serde(rename = "thick")]
|
||||||
|
Thick,
|
||||||
|
#[serde(rename = "tourney")]
|
||||||
|
Tourney,
|
||||||
|
#[serde(rename = "upsidedown")]
|
||||||
|
Upsidedown,
|
||||||
|
#[serde(rename = "vault")]
|
||||||
|
Vault,
|
||||||
|
#[serde(rename = "wizardsplaynetwork")]
|
||||||
|
WizardsPlayNetwork,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn weird_cards() -> Vec<String> {
|
||||||
|
// These all seem to be double faced cards with the same "card" on both sides.
|
||||||
|
vec![
|
||||||
|
"018830b2-dff9-45f3-9cc2-dc5b2eec0e54".to_string(),
|
||||||
|
"0489be0d-2117-46a8-97ab-31fe480685e2".to_string(),
|
||||||
|
"048ddb71-e9ea-4f11-9b8a-c53961cf3a2c".to_string(),
|
||||||
|
"087c3a0d-c710-4451-989e-596b55352184".to_string(),
|
||||||
|
"236e9bcf-ced2-4bee-8188-41dd94df02da".to_string(),
|
||||||
|
"36ea852d-ed2b-4c56-9b73-52dce8a3e520".to_string(),
|
||||||
|
"399bf36a-5901-437f-b5d3-32283cedbbcb".to_string(),
|
||||||
|
"3cb0824c-57cc-46bf-bd43-425d58b8a762".to_string(),
|
||||||
|
"fe388da5-9197-4d07-be7f-c49fcdf56dfa".to_string(),
|
||||||
|
"f973a1f3-6dcb-470d-89d2-6ddbf2426999".to_string(),
|
||||||
|
"f4e7b3a4-a346-4177-9cfe-0142b40ef4a6".to_string(),
|
||||||
|
"e25ce640-baf5-442b-8b75-d05dd9fb20dd".to_string(),
|
||||||
|
"dae8751c-4c72-4034-a192-a1e166f20246".to_string(),
|
||||||
|
"d74a72a2-d46a-41c2-a400-70571197b020".to_string(),
|
||||||
|
"d5f7a626-7b6b-41ba-a0f5-3aefe511b267".to_string(),
|
||||||
|
"d5dfd236-b1da-4552-b94f-ebf6bb9dafdf".to_string(),
|
||||||
|
"d002b29b-c3a6-4c91-86e1-96a50ce29966".to_string(),
|
||||||
|
"caf8d01d-07aa-43da-a26e-4a2ba3a76f2d".to_string(),
|
||||||
|
"c05c6c38-d204-458c-af17-4cf5efd2c7fc".to_string(),
|
||||||
|
"bffbe9ec-edbc-43ed-a3bf-60635e7e625c".to_string(),
|
||||||
|
"b96d6ea4-a3a4-4e33-be97-b3767f2bb63a".to_string(),
|
||||||
|
"acdb72e2-c000-4b92-b5ea-73115969020f".to_string(),
|
||||||
|
"aae84079-b65b-4132-86fb-e82503bb6c7b".to_string(),
|
||||||
|
"a724ebbc-0f77-42e9-95e0-b3e7cb130148".to_string(),
|
||||||
|
"a4a2dd5b-6143-4b8d-ae71-e148cf19b66c".to_string(),
|
||||||
|
"a129558c-45a1-441c-97f0-b70b4e9d8a56".to_string(),
|
||||||
|
"9f63277b-e139-46c8-b9e3-0cfb647f44cc".to_string(),
|
||||||
|
"9e69f9e0-4981-4fc0-955f-7ebe04264fca".to_string(),
|
||||||
|
"9d943cf2-0462-4f31-9a92-d76fe4971b17".to_string(),
|
||||||
|
"9cd6a16f-1eff-4624-8f7f-4d9e70a694bb".to_string(),
|
||||||
|
"9680a2d6-1d66-4f69-b400-a79fea4187d8".to_string(),
|
||||||
|
"94eea6e3-20bc-4dab-90ba-3113c120fb90".to_string(),
|
||||||
|
"94594d48-b728-4be6-9d7a-c67088df8acd".to_string(),
|
||||||
|
"3d89c9be-2489-47e4-8e53-f980c82442b4".to_string(),
|
||||||
|
"3e3f0bcd-0796-494d-bf51-94b33c1671e9".to_string(),
|
||||||
|
"4696f5de-fe5b-40df-a194-1a73b4c5150f".to_string(),
|
||||||
|
"4d227cd3-ebfe-4dd3-929a-4f8ff7c8981e".to_string(),
|
||||||
|
"5ab0412a-2b2f-430f-8830-002a42125148".to_string(),
|
||||||
|
"60c92f1b-0c78-4809-9365-e1ffa515cb4b".to_string(),
|
||||||
|
"6620b5f4-b1e5-4d1b-bbf2-c6ad9c8284c5".to_string(),
|
||||||
|
"67574bb4-c443-40fa-b7e6-05e9965c98b8".to_string(),
|
||||||
|
"6adadbc9-4a08-4c1d-adf7-edee73799d9e".to_string(),
|
||||||
|
"6c69ecd2-cb36-4628-802b-fd5ff7405f22".to_string(),
|
||||||
|
"76c343f5-6955-4ba2-a435-36d55182d1dd".to_string(),
|
||||||
|
"7e703632-5ed0-4509-a12b-594269f865f1".to_string(),
|
||||||
|
"82fa24fb-aecc-4c33-9e79-c29651ddafbe".to_string(),
|
||||||
|
"843b35ec-7b59-4a22-8fee-2e876a02306b".to_string(),
|
||||||
|
"8ae0caed-940d-45bc-9877-7cc014b2700e".to_string(),
|
||||||
|
"8b5341ab-85a6-44b2-b738-1110e699c02b".to_string(),
|
||||||
|
"8bcf942f-5afd-414e-a50d-00d884fe59da".to_string(),
|
||||||
|
"9052f5c7-ee3b-457d-97ca-ac6b4518997c".to_string(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::{BufRead, BufReader};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialise_nissa() {
|
||||||
|
let mut f = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
f.push("test_files/nissa.json");
|
||||||
|
assert!(f.exists());
|
||||||
|
let fc = fs::read_to_string(f).unwrap();
|
||||||
|
let _nissa: ScryfallCard = serde_json::from_str(&fc).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialise_black_lotus() {
|
||||||
|
let mut f = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
f.push("test_files/black_lotus.json");
|
||||||
|
assert!(f.exists());
|
||||||
|
let fc = fs::read_to_string(f).unwrap();
|
||||||
|
let _bl: ScryfallCard = serde_json::from_str(&fc).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialise_little_girl() {
|
||||||
|
let mut f = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
f.push("test_files/little_girl.json");
|
||||||
|
assert!(f.exists());
|
||||||
|
let fc = fs::read_to_string(f).unwrap();
|
||||||
|
let _lg: ScryfallCard = serde_json::from_str(&fc).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_line_by_line_with_bad_skip() {
|
||||||
|
// This function is uuuuuuugly and I'm sure a terrible way to go about things
|
||||||
|
// It is ever so slightly faster than the other one though!
|
||||||
|
let mut f = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
f.push("test_files/all-cards.json");
|
||||||
|
assert!(f.exists(), "You need to download the all-cards-... file from Scryfall bulk data. Can be found here: https://scryfall.com/docs/api/bulk-data and rename to all-cards.json");
|
||||||
|
let ac = fs::File::open(f).unwrap();
|
||||||
|
let reader = BufReader::new(ac);
|
||||||
|
let weird_cards = weird_cards();
|
||||||
|
for line in reader.lines().skip(1) {
|
||||||
|
let mut line = line.unwrap();
|
||||||
|
let c = line.pop().unwrap();
|
||||||
|
// this is so dumb...
|
||||||
|
if c == '}' {
|
||||||
|
line.push('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't look...
|
||||||
|
let mut skip = false;
|
||||||
|
for weird_card in &weird_cards {
|
||||||
|
if line.contains(weird_card) {
|
||||||
|
skip = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if skip {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let a_card: Result<ScryfallCard, serde_json::Error> =
|
||||||
|
serde_json::from_str(line.as_ref());
|
||||||
|
if let Err(error) = a_card {
|
||||||
|
println!("{:#?}", line);
|
||||||
|
println!("{:#?}", error);
|
||||||
|
}
|
||||||
|
//let a_card = a_card.unwrap();
|
||||||
|
//println!("{:?}", a_card.promo_types)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn deserialize_line_by_line() {
|
||||||
|
// This function is uuuuuuugly and I'm sure a terrible way to go about things
|
||||||
|
// It is ever so slightly faster than the other one though!
|
||||||
|
let mut f = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
f.push("test_files/all-cards.json");
|
||||||
|
assert!(f.exists(), "You need to download the all-cards-... file from Scryfall bulk data. Can be found here: https://scryfall.com/docs/api/bulk-data and rename to all-cards.json");
|
||||||
|
let ac = fs::File::open(f).unwrap();
|
||||||
|
let reader = BufReader::new(ac);
|
||||||
|
for line in reader.lines().skip(1) {
|
||||||
|
let mut line = line.unwrap();
|
||||||
|
let c = line.pop().unwrap();
|
||||||
|
// this is so dumb...
|
||||||
|
if c == '}' {
|
||||||
|
line.push('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let a_card: Result<ScryfallCard, serde_json::Error> =
|
||||||
|
serde_json::from_str(line.as_ref());
|
||||||
|
if let Err(error) = a_card {
|
||||||
|
println!("{:#?}", line);
|
||||||
|
println!("{:#?}", error);
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
//let a_card = a_card.unwrap();
|
||||||
|
//println!("{:?}", a_card.promo_types)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn deserialize_whole_file() {
|
||||||
|
let mut f = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
f.push("test_files/all-cards.json");
|
||||||
|
assert!(f.exists(), "You need to download the all-cards-... file from Scryfall bulk data. Can be found here: https://scryfall.com/docs/api/bulk-data and rename to all-cards.json");
|
||||||
|
|
||||||
|
let ac = fs::read_to_string(f).unwrap();
|
||||||
|
let _ac: Vec<ScryfallCard> = serde_json::from_str(&ac).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
95
scryfall_deser/src/download.rs
Normal file
95
scryfall_deser/src/download.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::io::Write;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
use ureq;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
struct ScryfallBulkData {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub uri: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub stype: String,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub download_uri: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
pub content_type: String,
|
||||||
|
pub content_encoding: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
struct ScryfallBulk {
|
||||||
|
pub object: String,
|
||||||
|
pub has_more: bool,
|
||||||
|
pub data: Vec<ScryfallBulkData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
||||||
|
pub enum ScryfallBulkType {
|
||||||
|
#[serde(rename = "oracle_cards")]
|
||||||
|
OracleCards,
|
||||||
|
#[serde(rename = "unique_artwork")]
|
||||||
|
UniqueArtwork,
|
||||||
|
#[serde(rename = "default_cards")]
|
||||||
|
DefaultCards,
|
||||||
|
#[serde(rename = "all_cards")]
|
||||||
|
AllCards,
|
||||||
|
#[serde(rename = "rulings")]
|
||||||
|
Rulings,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCRYFALL_BULK_API: &str = "https://api.scryfall.com/bulk-data";
|
||||||
|
|
||||||
|
pub fn download_latest(
|
||||||
|
stype: ScryfallBulkType,
|
||||||
|
mut dest_file: &NamedTempFile,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let bulk_body: ScryfallBulk = ureq::get(SCRYFALL_BULK_API)
|
||||||
|
.header("User-Agent", "Arthur's Card Finger Testing v0.1")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.call()?
|
||||||
|
.body_mut()
|
||||||
|
.read_json::<ScryfallBulk>()?;
|
||||||
|
|
||||||
|
let mut download_uri = String::new();
|
||||||
|
for scryfall_bulk in bulk_body.data {
|
||||||
|
if serde_json::to_string(&stype)
|
||||||
|
.unwrap()
|
||||||
|
.contains(&scryfall_bulk.stype)
|
||||||
|
{
|
||||||
|
download_uri = scryfall_bulk.download_uri;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(!download_uri.is_empty());
|
||||||
|
return Ok(());
|
||||||
|
// Just while testing - don't need to download 150MB every time...
|
||||||
|
let cards_response = ureq::get(download_uri)
|
||||||
|
.header("User-Agent", "Arthur's Card Finger Testing v0.1")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.call()?
|
||||||
|
.body_mut()
|
||||||
|
.with_config()
|
||||||
|
.limit(700 * 1024 * 1024)
|
||||||
|
.read_to_string()?;
|
||||||
|
write!(dest_file, "{}", cards_response)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::io::{Seek, SeekFrom};
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn get_scryfall_bulk_page() {
|
||||||
|
let mut file = NamedTempFile::new().unwrap();
|
||||||
|
let _ = download_latest(ScryfallBulkType::OracleCards, &file);
|
||||||
|
let file_size = file.seek(SeekFrom::End(0)).unwrap();
|
||||||
|
assert!(file_size > 4092);
|
||||||
|
println!("File size: {}", file_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,788 +1,15 @@
|
|||||||
use chrono::NaiveDate;
|
mod download;
|
||||||
use serde::Deserialize;
|
pub use crate::download::download_latest;
|
||||||
use serde_json::Value;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
// Info from here:
|
mod deser;
|
||||||
// https://scryfall.com/docs/api/cards
|
pub use crate::deser::ScryfallCard;
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
#[serde(deny_unknown_fields)]
|
|
||||||
struct ScryfallCard {
|
|
||||||
// Core Card Fields
|
|
||||||
pub arena_id: Option<u64>,
|
|
||||||
pub id: Uuid,
|
|
||||||
pub lang: String,
|
|
||||||
pub mtgo_id: Option<u64>,
|
|
||||||
pub mtgo_foil_id: Option<u64>,
|
|
||||||
pub multiverse_ids: Option<Vec<u64>>,
|
|
||||||
pub tcgplayer_id: Option<u64>,
|
|
||||||
pub tcgplayer_etched_id: Option<u64>,
|
|
||||||
pub cardmarket_id: Option<u64>,
|
|
||||||
pub object: String,
|
|
||||||
pub layout: String, // Perhaps some kind of enum of these: https://scryfall.com/docs/api/layouts?
|
|
||||||
pub oracle_id: Option<Uuid>,
|
|
||||||
pub prints_search_uri: String, // URI
|
|
||||||
pub rulings_uri: String, // URI
|
|
||||||
pub scryfall_uri: String, // URI
|
|
||||||
pub uri: String, // URI
|
|
||||||
|
|
||||||
// Gameplay Fields
|
mod db;
|
||||||
// https://scryfall.com/docs/api/cards#gameplay-fields
|
pub use db::{
|
||||||
pub all_parts: Option<Vec<ScryfallRelatedCardObject>>,
|
find_matching_cards, find_matching_cards_scryfall_style, get_all_card_names,
|
||||||
pub card_faces: Option<Vec<ScryfallCardFaceObject>>,
|
get_all_lowercase_card_names, get_all_mtg_words, get_card_by_name, init_db,
|
||||||
|
update_db_with_file, GetNameType,
|
||||||
|
};
|
||||||
|
|
||||||
// NOTE: Much of the next is a repeat of what's in the ScryfallCardFaceObject if you change something here, change something there
|
mod utils;
|
||||||
// NOTE: Probably a bad idea to rename color -> colour just for the sake
|
pub use utils::get_local_cache_folder;
|
||||||
pub cmc: Option<f64>, // TODO: Make this a proper Decimal - see "Little Girl" card for example of cmc of 0.5
|
|
||||||
#[serde(rename = "color_identity")]
|
|
||||||
pub colour_identity: Option<Vec<Colour>>,
|
|
||||||
#[serde(rename = "color_indicator")]
|
|
||||||
pub colour_indicator: Option<Vec<Colour>>,
|
|
||||||
#[serde(rename = "colors")]
|
|
||||||
pub colours: Option<Vec<Colour>>,
|
|
||||||
pub edhrec_rank: Option<u64>,
|
|
||||||
pub defense: Option<String>,
|
|
||||||
pub hand_modifier: Option<String>,
|
|
||||||
pub keywords: Vec<String>, // Words like "Flying"
|
|
||||||
pub legalities: FormatLegalities,
|
|
||||||
pub life_modifier: Option<String>,
|
|
||||||
pub loyalty: Option<String>,
|
|
||||||
pub mana_cost: Option<String>,
|
|
||||||
pub name: String,
|
|
||||||
pub oracle_text: Option<String>,
|
|
||||||
pub penny_rank: Option<u64>,
|
|
||||||
pub power: Option<String>,
|
|
||||||
pub produced_mana: Option<Vec<Colour>>,
|
|
||||||
pub reserved: bool,
|
|
||||||
pub toughness: Option<String>,
|
|
||||||
pub type_line: String,
|
|
||||||
|
|
||||||
// Print Fields
|
|
||||||
// https://scryfall.com/docs/api/cards#print-fields
|
|
||||||
pub artist: Option<String>,
|
|
||||||
pub artist_ids: Option<Vec<String>>,
|
|
||||||
pub attraction_lights: Option<Vec<u8>>,
|
|
||||||
pub booster: bool,
|
|
||||||
#[serde(rename = "border_color")]
|
|
||||||
pub border_colour: BorderColour,
|
|
||||||
pub card_back_id: Option<Uuid>, // Scryfall docs says this should not be null, but ZHS Growing Rites of Itlimoc seems to not have one... maybe it's the back side?
|
|
||||||
pub collector_number: String,
|
|
||||||
pub content_warning: Option<bool>,
|
|
||||||
pub digital: bool,
|
|
||||||
pub finishes: Vec<Finish>,
|
|
||||||
#[serde(rename = "flavor_name")]
|
|
||||||
pub flavour_name: Option<String>,
|
|
||||||
#[serde(rename = "flavor_text")]
|
|
||||||
pub flavour_text: Option<String>,
|
|
||||||
pub frame_effects: Option<Vec<FrameEffect>>,
|
|
||||||
pub frame: Frame,
|
|
||||||
pub full_art: bool,
|
|
||||||
pub games: Vec<Game>,
|
|
||||||
pub highres_image: bool,
|
|
||||||
pub illustration_id: Option<Uuid>,
|
|
||||||
pub image_status: ImageStatus,
|
|
||||||
pub image_uris: Option<ImageURIs>,
|
|
||||||
pub oversized: bool,
|
|
||||||
pub prices: Prices,
|
|
||||||
pub printed_name: Option<String>,
|
|
||||||
pub printed_text: Option<String>,
|
|
||||||
pub printed_type_line: Option<String>,
|
|
||||||
pub promo: bool,
|
|
||||||
pub promo_types: Option<Vec<PromoTypes>>,
|
|
||||||
pub purchase_uris: Option<PurchaseUris>,
|
|
||||||
pub rarity: Rarity,
|
|
||||||
pub related_uris: Value, // TODO: - list all the URIs? Maybe? Who cares?
|
|
||||||
pub released_at: NaiveDate,
|
|
||||||
pub reprint: bool,
|
|
||||||
pub scryfall_set_uri: String, // URI
|
|
||||||
pub set_name: String,
|
|
||||||
pub set_search_uri: String, // URI
|
|
||||||
pub set_type: SetType,
|
|
||||||
pub set_uri: String, // URI
|
|
||||||
pub set: String,
|
|
||||||
pub set_id: Uuid,
|
|
||||||
pub story_spotlight: bool,
|
|
||||||
pub textless: bool,
|
|
||||||
pub variation: bool,
|
|
||||||
pub variation_of: Option<Uuid>,
|
|
||||||
pub security_stamp: Option<SecurityStamp>,
|
|
||||||
pub watermark: Option<String>,
|
|
||||||
pub preview: Option<Preview>,
|
|
||||||
|
|
||||||
|
|
||||||
// These aren't in the Scryfall docs, but some cards do have 'em
|
|
||||||
pub foil: Option<bool>,
|
|
||||||
pub nonfoil: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://scryfall.com/docs/api/cards#card-face-objects
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct ScryfallCardFaceObject {
|
|
||||||
pub artist: Option<String>,
|
|
||||||
pub artist_id: Option<Uuid>, // UUID
|
|
||||||
pub cmc: Option<f64>, // TODO: Make this a proper Decimal - see "Little Girl" card for example of cmc of 0.5
|
|
||||||
#[serde(rename = "color_identity")]
|
|
||||||
pub colour_identity: Option<Vec<Colour>>,
|
|
||||||
#[serde(rename = "color_indicator")]
|
|
||||||
pub colour_indicator: Option<Vec<Colour>>,
|
|
||||||
#[serde(rename = "colors")]
|
|
||||||
pub colours: Option<Vec<Colour>>,
|
|
||||||
pub defense: Option<String>,
|
|
||||||
pub flavour_text: Option<String>,
|
|
||||||
pub illustration_id: Option<Uuid>,
|
|
||||||
pub image_uris: Option<ImageURIs>,
|
|
||||||
pub layout: Option<String>,
|
|
||||||
pub loyalty: Option<String>,
|
|
||||||
pub mana_cost: Option<String>,
|
|
||||||
pub name: String,
|
|
||||||
pub object: String,
|
|
||||||
pub oracle_id: Option<Uuid>,
|
|
||||||
pub oracle_text: Option<String>,
|
|
||||||
pub power: Option<String>,
|
|
||||||
pub printed_name: Option<String>,
|
|
||||||
pub printed_text: Option<String>,
|
|
||||||
pub printed_type_line: Option<String>,
|
|
||||||
pub toughness: Option<String>,
|
|
||||||
pub type_line: Option<String>,
|
|
||||||
pub watermark: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://scryfall.com/docs/api/cards#related-card-objects
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct ScryfallRelatedCardObject {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub object: String, // Always "related_card"
|
|
||||||
pub component: Component,
|
|
||||||
pub name: String,
|
|
||||||
pub type_line: String,
|
|
||||||
pub uri: String, // URI
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, PartialEq, Debug)]
|
|
||||||
enum Colour {
|
|
||||||
#[serde(rename = "W")]
|
|
||||||
White,
|
|
||||||
#[serde(rename = "U")]
|
|
||||||
Blue,
|
|
||||||
#[serde(rename = "B")]
|
|
||||||
Black,
|
|
||||||
#[serde(rename = "R")]
|
|
||||||
Red,
|
|
||||||
#[serde(rename = "G")]
|
|
||||||
Green,
|
|
||||||
#[serde(rename = "C")] // I don't think it's meant to work like this... but eh
|
|
||||||
Colourless,
|
|
||||||
#[serde(rename = "T")] // See "Sole Performer"
|
|
||||||
Tap,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
enum Legality {
|
|
||||||
#[serde(rename = "legal")]
|
|
||||||
Legal,
|
|
||||||
#[serde(rename = "not_legal")]
|
|
||||||
NotLegal,
|
|
||||||
#[serde(rename = "banned")]
|
|
||||||
Banned,
|
|
||||||
#[serde(rename = "restricted")]
|
|
||||||
Restricted,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct FormatLegalities {
|
|
||||||
standard: Legality,
|
|
||||||
future: Legality,
|
|
||||||
historic: Legality,
|
|
||||||
timeless: Legality,
|
|
||||||
gladiator: Legality,
|
|
||||||
pioneer: Legality,
|
|
||||||
explorer: Legality,
|
|
||||||
modern: Legality,
|
|
||||||
legacy: Legality,
|
|
||||||
pauper: Legality,
|
|
||||||
vintage: Legality,
|
|
||||||
penny: Legality,
|
|
||||||
commander: Legality,
|
|
||||||
oathbreaker: Legality,
|
|
||||||
standardbrawl: Legality,
|
|
||||||
brawl: Legality,
|
|
||||||
alchemy: Legality,
|
|
||||||
paupercommander: Legality,
|
|
||||||
duel: Legality,
|
|
||||||
oldschool: Legality,
|
|
||||||
premodern: Legality,
|
|
||||||
predh: Legality,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
enum BorderColour {
|
|
||||||
#[serde(rename = "black")]
|
|
||||||
Black,
|
|
||||||
#[serde(rename = "white")]
|
|
||||||
White,
|
|
||||||
#[serde(rename = "borderless")]
|
|
||||||
Borderless,
|
|
||||||
#[serde(rename = "yellow")]
|
|
||||||
Yellow,
|
|
||||||
#[serde(rename = "silver")]
|
|
||||||
Silver,
|
|
||||||
#[serde(rename = "gold")]
|
|
||||||
Gold,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
enum Finish {
|
|
||||||
#[serde(rename = "foil")]
|
|
||||||
Foil,
|
|
||||||
#[serde(rename = "nonfoil")]
|
|
||||||
NonFoil,
|
|
||||||
#[serde(rename = "etched")]
|
|
||||||
Etched,
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://scryfall.com/docs/api/frames#frames
|
|
||||||
// This is probably dumb...
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
enum Frame {
|
|
||||||
#[serde(rename = "1993")]
|
|
||||||
NinetyThree,
|
|
||||||
#[serde(rename = "1997")]
|
|
||||||
NinetySeven,
|
|
||||||
#[serde(rename = "2003")]
|
|
||||||
OhThree,
|
|
||||||
#[serde(rename = "2015")]
|
|
||||||
Fifteen,
|
|
||||||
#[serde(rename = "future")]
|
|
||||||
Future,
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://scryfall.com/docs/api/frames#frame-effects
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
enum FrameEffect {
|
|
||||||
#[serde(rename = "legendary")]
|
|
||||||
Legendary,
|
|
||||||
#[serde(rename = "miracle")]
|
|
||||||
Miracle,
|
|
||||||
#[serde(rename = "enchantment")]
|
|
||||||
Enchantment,
|
|
||||||
#[serde(rename = "draft")]
|
|
||||||
Draft,
|
|
||||||
#[serde(rename = "devoid")]
|
|
||||||
Devoid,
|
|
||||||
#[serde(rename = "tombstone")]
|
|
||||||
Tombstone,
|
|
||||||
#[serde(rename = "colorshifted")]
|
|
||||||
Colourshifted,
|
|
||||||
#[serde(rename = "inverted")]
|
|
||||||
Inverted,
|
|
||||||
#[serde(rename = "sunmoondfc")]
|
|
||||||
SunMoonDFC,
|
|
||||||
#[serde(rename = "compasslanddfc")]
|
|
||||||
CompassLandDFC,
|
|
||||||
#[serde(rename = "originpwdfc")]
|
|
||||||
OriginPwDFC,
|
|
||||||
#[serde(rename = "mooneldrazidfc")]
|
|
||||||
MoonEldraziDFC,
|
|
||||||
#[serde(rename = "waxingandwaningmoondfc")]
|
|
||||||
WaxingAndWaningMoonDFC,
|
|
||||||
#[serde(rename = "showcase")]
|
|
||||||
Showcase,
|
|
||||||
#[serde(rename = "extendedart")]
|
|
||||||
ExtendedArt,
|
|
||||||
#[serde(rename = "companion")]
|
|
||||||
Companion,
|
|
||||||
#[serde(rename = "etched")]
|
|
||||||
Etched,
|
|
||||||
#[serde(rename = "snow")]
|
|
||||||
Snow,
|
|
||||||
#[serde(rename = "lesson")]
|
|
||||||
Lesson,
|
|
||||||
#[serde(rename = "shatteredglass")]
|
|
||||||
ShatteredGlass,
|
|
||||||
#[serde(rename = "convertdfc")]
|
|
||||||
ConvertDFC,
|
|
||||||
#[serde(rename = "fandfc")]
|
|
||||||
FanDFC,
|
|
||||||
#[serde(rename = "upsidedowndfc")]
|
|
||||||
UpsideDownDFC,
|
|
||||||
#[serde(rename = "spree")]
|
|
||||||
Spree,
|
|
||||||
#[serde(rename = "fullart")]
|
|
||||||
FullArt,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
enum Game {
|
|
||||||
#[serde(rename = "paper")]
|
|
||||||
Paper,
|
|
||||||
#[serde(rename = "mtgo")]
|
|
||||||
Mtgo,
|
|
||||||
#[serde(rename = "arena")]
|
|
||||||
Arena,
|
|
||||||
#[serde(rename = "astral")]
|
|
||||||
Astral,
|
|
||||||
#[serde(rename = "sega")]
|
|
||||||
Sega,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
enum ImageStatus {
|
|
||||||
#[serde(rename = "missing")]
|
|
||||||
Missing,
|
|
||||||
#[serde(rename = "placeholder")]
|
|
||||||
Placeholder,
|
|
||||||
#[serde(rename = "lowres")]
|
|
||||||
LowResolution,
|
|
||||||
#[serde(rename = "highres_scan")]
|
|
||||||
HighResolutionScan,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct ImageURIs {
|
|
||||||
png: Option<String>,
|
|
||||||
border_crop: Option<String>,
|
|
||||||
art_crop: Option<String>,
|
|
||||||
large: Option<String>,
|
|
||||||
normal: Option<String>,
|
|
||||||
small: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct Preview {
|
|
||||||
pub previewed_at: Option<NaiveDate>,
|
|
||||||
pub source_uri: Option<String>, // URI
|
|
||||||
pub source: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct Prices {
|
|
||||||
usd: Option<String>, // TODO Convert to f64?
|
|
||||||
usd_foil: Option<String>,
|
|
||||||
usd_etched: Option<String>,
|
|
||||||
eur: Option<String>,
|
|
||||||
eur_foil: Option<String>,
|
|
||||||
tix: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
enum Rarity {
|
|
||||||
#[serde(rename = "common")]
|
|
||||||
Common,
|
|
||||||
#[serde(rename = "uncommon")]
|
|
||||||
Uncommon,
|
|
||||||
#[serde(rename = "rare")]
|
|
||||||
Rare,
|
|
||||||
#[serde(rename = "special")]
|
|
||||||
Special,
|
|
||||||
#[serde(rename = "mythic")]
|
|
||||||
Mythic,
|
|
||||||
#[serde(rename = "bonus")]
|
|
||||||
Bonus,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct PurchaseUris {
|
|
||||||
tcgplayer: String, // Option?
|
|
||||||
cardmarket: String,
|
|
||||||
cardhoarder: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
enum SecurityStamp {
|
|
||||||
#[serde(rename = "oval")]
|
|
||||||
Oval,
|
|
||||||
#[serde(rename = "triangle")]
|
|
||||||
Triangle,
|
|
||||||
#[serde(rename = "acorn")]
|
|
||||||
Acorn,
|
|
||||||
#[serde(rename = "circle")]
|
|
||||||
Circle,
|
|
||||||
#[serde(rename = "arena")]
|
|
||||||
Arena,
|
|
||||||
#[serde(rename = "heart")]
|
|
||||||
Heart,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
enum Component {
|
|
||||||
#[serde(rename = "token")]
|
|
||||||
Token,
|
|
||||||
#[serde(rename = "meld_part")]
|
|
||||||
MeldPart,
|
|
||||||
#[serde(rename = "meld_result")]
|
|
||||||
MeldResult,
|
|
||||||
#[serde(rename = "combo_piece")]
|
|
||||||
ComboPiece,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
enum SetType {
|
|
||||||
#[serde(rename = "alchemy")]
|
|
||||||
Alchemy,
|
|
||||||
#[serde(rename = "archenemy")]
|
|
||||||
Archenemy,
|
|
||||||
#[serde(rename = "arsenal")]
|
|
||||||
Arsenal,
|
|
||||||
#[serde(rename = "box")]
|
|
||||||
Box,
|
|
||||||
#[serde(rename = "commander")]
|
|
||||||
Commander,
|
|
||||||
#[serde(rename = "core")]
|
|
||||||
Core,
|
|
||||||
#[serde(rename = "draft_innovation")]
|
|
||||||
DraftInnovation,
|
|
||||||
#[serde(rename = "duel_deck")]
|
|
||||||
DuelDeck,
|
|
||||||
#[serde(rename = "expansion")]
|
|
||||||
Expansion,
|
|
||||||
#[serde(rename = "from_the_vault")]
|
|
||||||
FromTheVault,
|
|
||||||
#[serde(rename = "funny")]
|
|
||||||
Funny,
|
|
||||||
#[serde(rename = "masterpiece")]
|
|
||||||
Masterpiece,
|
|
||||||
#[serde(rename = "masters")]
|
|
||||||
Masters,
|
|
||||||
#[serde(rename = "memorabilia")]
|
|
||||||
Memorabilia,
|
|
||||||
#[serde(rename = "minigame")]
|
|
||||||
Minigame,
|
|
||||||
#[serde(rename = "planechase")]
|
|
||||||
Planechase,
|
|
||||||
#[serde(rename = "premium_deck")]
|
|
||||||
PremiumDeck,
|
|
||||||
#[serde(rename = "promo")]
|
|
||||||
Promo,
|
|
||||||
#[serde(rename = "spellbook")]
|
|
||||||
SpellBook,
|
|
||||||
#[serde(rename = "starter")]
|
|
||||||
Starter,
|
|
||||||
#[serde(rename = "token")]
|
|
||||||
Token,
|
|
||||||
#[serde(rename = "treasure_chest")]
|
|
||||||
TreasureChest,
|
|
||||||
#[serde(rename = "vanguard")]
|
|
||||||
Vanguard,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Complete this
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
enum PromoTypes {
|
|
||||||
#[serde(rename = "alchemy")]
|
|
||||||
Alchemy,
|
|
||||||
#[serde(rename = "arenaleague")]
|
|
||||||
ArenaLeague,
|
|
||||||
#[serde(rename = "beginnerbox")]
|
|
||||||
BeginnerBox,
|
|
||||||
#[serde(rename = "boosterfun")]
|
|
||||||
BoosterFun,
|
|
||||||
#[serde(rename = "boxtopper")]
|
|
||||||
BoxTopper,
|
|
||||||
#[serde(rename = "brawldeck")]
|
|
||||||
BrawlDeck,
|
|
||||||
#[serde(rename = "bundle")]
|
|
||||||
Bundle,
|
|
||||||
#[serde(rename = "buyabox")]
|
|
||||||
BuyABox,
|
|
||||||
#[serde(rename = "confettifoil")]
|
|
||||||
ConfettiFoil,
|
|
||||||
#[serde(rename = "convention")]
|
|
||||||
Convention,
|
|
||||||
#[serde(rename = "datestamped")]
|
|
||||||
DateStamped,
|
|
||||||
#[serde(rename = "dossier")]
|
|
||||||
Dossier,
|
|
||||||
#[serde(rename = "doublerainbow")]
|
|
||||||
DoubleRainbow,
|
|
||||||
#[serde(rename = "embossed")]
|
|
||||||
Embossed,
|
|
||||||
#[serde(rename = "event")]
|
|
||||||
Event,
|
|
||||||
#[serde(rename = "fnm")]
|
|
||||||
Fnm,
|
|
||||||
#[serde(rename = "gameday")]
|
|
||||||
GameDay,
|
|
||||||
#[serde(rename = "godzillaseries")]
|
|
||||||
GodzillaSeries,
|
|
||||||
#[serde(rename = "halofoil")]
|
|
||||||
HaloFoil,
|
|
||||||
#[serde(rename = "imagine")]
|
|
||||||
Imagine,
|
|
||||||
#[serde(rename = "instore")]
|
|
||||||
InStore,
|
|
||||||
#[serde(rename = "intropack")]
|
|
||||||
IntroPack,
|
|
||||||
#[serde(rename = "invisibleink")]
|
|
||||||
InvisibleInk,
|
|
||||||
#[serde(rename = "judgegift")]
|
|
||||||
JudgeGift,
|
|
||||||
#[serde(rename = "league")]
|
|
||||||
League,
|
|
||||||
#[serde(rename = "magnified")]
|
|
||||||
Magnified,
|
|
||||||
#[serde(rename = "manafoil")]
|
|
||||||
ManaFoil,
|
|
||||||
#[serde(rename = "mediainsert")]
|
|
||||||
MediaInsert,
|
|
||||||
#[serde(rename = "planeswalkerdeck")]
|
|
||||||
PlaneswalkerDeck,
|
|
||||||
#[serde(rename = "plastic")]
|
|
||||||
Plastic,
|
|
||||||
#[serde(rename = "playerrewards")]
|
|
||||||
PlayerRewards,
|
|
||||||
#[serde(rename = "playtest")]
|
|
||||||
Playtest,
|
|
||||||
#[serde(rename = "poster")]
|
|
||||||
Poster,
|
|
||||||
#[serde(rename = "prerelease")]
|
|
||||||
Prerelease,
|
|
||||||
#[serde(rename = "premiereshop")]
|
|
||||||
PremiereShop,
|
|
||||||
#[serde(rename = "promopack")]
|
|
||||||
PromoPack,
|
|
||||||
#[serde(rename = "rainbowfoil")]
|
|
||||||
RainbowFoil,
|
|
||||||
#[serde(rename = "ravnicacity")]
|
|
||||||
RavnicaCity,
|
|
||||||
#[serde(rename = "rebalanced")]
|
|
||||||
Rebalanced,
|
|
||||||
#[serde(rename = "release")]
|
|
||||||
Release,
|
|
||||||
#[serde(rename = "resale")]
|
|
||||||
Resale,
|
|
||||||
#[serde(rename = "ripplefoil")]
|
|
||||||
RippleFoil,
|
|
||||||
#[serde(rename = "setpromo")]
|
|
||||||
SetPromo,
|
|
||||||
#[serde(rename = "serialized")]
|
|
||||||
Serialised,
|
|
||||||
#[serde(rename = "silverfoil")]
|
|
||||||
SilverFoil,
|
|
||||||
#[serde(rename = "sldbonus")]
|
|
||||||
SldBonus,
|
|
||||||
#[serde(rename = "stamped")]
|
|
||||||
Stamped,
|
|
||||||
#[serde(rename = "startercollection")]
|
|
||||||
StarterCollection,
|
|
||||||
#[serde(rename = "starterdeck")]
|
|
||||||
StarterDeck,
|
|
||||||
#[serde(rename = "stepandcompleat")]
|
|
||||||
StepAndCompleat,
|
|
||||||
#[serde(rename = "surgefoil")]
|
|
||||||
SurgeFoil,
|
|
||||||
#[serde(rename = "textured")]
|
|
||||||
Textured,
|
|
||||||
#[serde(rename = "themepack")]
|
|
||||||
ThemePack,
|
|
||||||
#[serde(rename = "thick")]
|
|
||||||
Thick,
|
|
||||||
#[serde(rename = "tourney")]
|
|
||||||
Tourney,
|
|
||||||
#[serde(rename = "upsidedown")]
|
|
||||||
Upsidedown,
|
|
||||||
#[serde(rename = "vault")]
|
|
||||||
Vault,
|
|
||||||
#[serde(rename = "wizardsplaynetwork")]
|
|
||||||
WizardsPlayNetwork,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn weird_cards() -> Vec<String> {
|
|
||||||
// These all seem to be double faced cards with the same "card" on both sides.
|
|
||||||
vec![
|
|
||||||
"018830b2-dff9-45f3-9cc2-dc5b2eec0e54".to_string(),
|
|
||||||
"0489be0d-2117-46a8-97ab-31fe480685e2".to_string(),
|
|
||||||
"048ddb71-e9ea-4f11-9b8a-c53961cf3a2c".to_string(),
|
|
||||||
"087c3a0d-c710-4451-989e-596b55352184".to_string(),
|
|
||||||
"236e9bcf-ced2-4bee-8188-41dd94df02da".to_string(),
|
|
||||||
"36ea852d-ed2b-4c56-9b73-52dce8a3e520".to_string(),
|
|
||||||
"399bf36a-5901-437f-b5d3-32283cedbbcb".to_string(),
|
|
||||||
"3cb0824c-57cc-46bf-bd43-425d58b8a762".to_string(),
|
|
||||||
"fe388da5-9197-4d07-be7f-c49fcdf56dfa".to_string(),
|
|
||||||
"f973a1f3-6dcb-470d-89d2-6ddbf2426999".to_string(),
|
|
||||||
"f4e7b3a4-a346-4177-9cfe-0142b40ef4a6".to_string(),
|
|
||||||
"e25ce640-baf5-442b-8b75-d05dd9fb20dd".to_string(),
|
|
||||||
"dae8751c-4c72-4034-a192-a1e166f20246".to_string(),
|
|
||||||
"d74a72a2-d46a-41c2-a400-70571197b020".to_string(),
|
|
||||||
"d5f7a626-7b6b-41ba-a0f5-3aefe511b267".to_string(),
|
|
||||||
"d5dfd236-b1da-4552-b94f-ebf6bb9dafdf".to_string(),
|
|
||||||
"d002b29b-c3a6-4c91-86e1-96a50ce29966".to_string(),
|
|
||||||
"caf8d01d-07aa-43da-a26e-4a2ba3a76f2d".to_string(),
|
|
||||||
"c05c6c38-d204-458c-af17-4cf5efd2c7fc".to_string(),
|
|
||||||
"bffbe9ec-edbc-43ed-a3bf-60635e7e625c".to_string(),
|
|
||||||
"b96d6ea4-a3a4-4e33-be97-b3767f2bb63a".to_string(),
|
|
||||||
"acdb72e2-c000-4b92-b5ea-73115969020f".to_string(),
|
|
||||||
"aae84079-b65b-4132-86fb-e82503bb6c7b".to_string(),
|
|
||||||
"a724ebbc-0f77-42e9-95e0-b3e7cb130148".to_string(),
|
|
||||||
"a4a2dd5b-6143-4b8d-ae71-e148cf19b66c".to_string(),
|
|
||||||
"a129558c-45a1-441c-97f0-b70b4e9d8a56".to_string(),
|
|
||||||
"9f63277b-e139-46c8-b9e3-0cfb647f44cc".to_string(),
|
|
||||||
"9e69f9e0-4981-4fc0-955f-7ebe04264fca".to_string(),
|
|
||||||
"9d943cf2-0462-4f31-9a92-d76fe4971b17".to_string(),
|
|
||||||
"9cd6a16f-1eff-4624-8f7f-4d9e70a694bb".to_string(),
|
|
||||||
"9680a2d6-1d66-4f69-b400-a79fea4187d8".to_string(),
|
|
||||||
"94eea6e3-20bc-4dab-90ba-3113c120fb90".to_string(),
|
|
||||||
"94594d48-b728-4be6-9d7a-c67088df8acd".to_string(),
|
|
||||||
"3d89c9be-2489-47e4-8e53-f980c82442b4".to_string(),
|
|
||||||
"3e3f0bcd-0796-494d-bf51-94b33c1671e9".to_string(),
|
|
||||||
"4696f5de-fe5b-40df-a194-1a73b4c5150f".to_string(),
|
|
||||||
"4d227cd3-ebfe-4dd3-929a-4f8ff7c8981e".to_string(),
|
|
||||||
"5ab0412a-2b2f-430f-8830-002a42125148".to_string(),
|
|
||||||
"60c92f1b-0c78-4809-9365-e1ffa515cb4b".to_string(),
|
|
||||||
"6620b5f4-b1e5-4d1b-bbf2-c6ad9c8284c5".to_string(),
|
|
||||||
"67574bb4-c443-40fa-b7e6-05e9965c98b8".to_string(),
|
|
||||||
"6adadbc9-4a08-4c1d-adf7-edee73799d9e".to_string(),
|
|
||||||
"6c69ecd2-cb36-4628-802b-fd5ff7405f22".to_string(),
|
|
||||||
"76c343f5-6955-4ba2-a435-36d55182d1dd".to_string(),
|
|
||||||
"7e703632-5ed0-4509-a12b-594269f865f1".to_string(),
|
|
||||||
"82fa24fb-aecc-4c33-9e79-c29651ddafbe".to_string(),
|
|
||||||
"843b35ec-7b59-4a22-8fee-2e876a02306b".to_string(),
|
|
||||||
"8ae0caed-940d-45bc-9877-7cc014b2700e".to_string(),
|
|
||||||
"8b5341ab-85a6-44b2-b738-1110e699c02b".to_string(),
|
|
||||||
"8bcf942f-5afd-414e-a50d-00d884fe59da".to_string(),
|
|
||||||
"9052f5c7-ee3b-457d-97ca-ac6b4518997c".to_string(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use std::fs;
|
|
||||||
use std::io::{BufRead, BufReader};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deserialise_nissa() {
|
|
||||||
let mut f = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
||||||
f.push("test_files/nissa.json");
|
|
||||||
assert!(f.exists());
|
|
||||||
let fc = fs::read_to_string(f).unwrap();
|
|
||||||
let _nissa: ScryfallCard = serde_json::from_str(&fc).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deserialise_black_lotus() {
|
|
||||||
let mut f = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
||||||
f.push("test_files/black_lotus.json");
|
|
||||||
assert!(f.exists());
|
|
||||||
let fc = fs::read_to_string(f).unwrap();
|
|
||||||
let _bl: ScryfallCard = serde_json::from_str(&fc).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deserialise_little_girl() {
|
|
||||||
let mut f = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
||||||
f.push("test_files/little_girl.json");
|
|
||||||
assert!(f.exists());
|
|
||||||
let fc = fs::read_to_string(f).unwrap();
|
|
||||||
let _lg: ScryfallCard = serde_json::from_str(&fc).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deserialize_line_by_line_with_bad_skip() {
|
|
||||||
// This function is uuuuuuugly and I'm sure a terrible way to go about things
|
|
||||||
// It is ever so slightly faster than the other one though!
|
|
||||||
let mut f = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
||||||
f.push("test_files/all-cards.json");
|
|
||||||
assert!(f.exists(), "You need to download the all-cards-... file from Scryfall bulk data. Can be found here: https://scryfall.com/docs/api/bulk-data and rename to all-cards.json");
|
|
||||||
let ac = fs::File::open(f).unwrap();
|
|
||||||
let reader = BufReader::new(ac);
|
|
||||||
let weird_cards = weird_cards();
|
|
||||||
for line in reader.lines().skip(1) {
|
|
||||||
let mut line = line.unwrap();
|
|
||||||
let c = line.pop().unwrap();
|
|
||||||
// this is so dumb...
|
|
||||||
if c == '}' {
|
|
||||||
line.push('}');
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't look...
|
|
||||||
let mut skip = false;
|
|
||||||
for weird_card in &weird_cards {
|
|
||||||
if line.contains(weird_card) {
|
|
||||||
skip = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if skip {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if line.is_empty() {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let a_card: Result<ScryfallCard, serde_json::Error> =
|
|
||||||
serde_json::from_str(line.as_ref());
|
|
||||||
if let Err(error) = a_card {
|
|
||||||
println!("{:#?}", line);
|
|
||||||
println!("{:#?}", error);
|
|
||||||
}
|
|
||||||
//let a_card = a_card.unwrap();
|
|
||||||
//println!("{:?}", a_card.promo_types)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore]
|
|
||||||
fn deserialize_line_by_line() {
|
|
||||||
// This function is uuuuuuugly and I'm sure a terrible way to go about things
|
|
||||||
// It is ever so slightly faster than the other one though!
|
|
||||||
let mut f = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
||||||
f.push("test_files/all-cards.json");
|
|
||||||
assert!(f.exists(), "You need to download the all-cards-... file from Scryfall bulk data. Can be found here: https://scryfall.com/docs/api/bulk-data and rename to all-cards.json");
|
|
||||||
let ac = fs::File::open(f).unwrap();
|
|
||||||
let reader = BufReader::new(ac);
|
|
||||||
for line in reader.lines().skip(1) {
|
|
||||||
let mut line = line.unwrap();
|
|
||||||
let c = line.pop().unwrap();
|
|
||||||
// this is so dumb...
|
|
||||||
if c == '}' {
|
|
||||||
line.push('}');
|
|
||||||
}
|
|
||||||
|
|
||||||
if line.is_empty() {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let a_card: Result<ScryfallCard, serde_json::Error> =
|
|
||||||
serde_json::from_str(line.as_ref());
|
|
||||||
if let Err(error) = a_card {
|
|
||||||
println!("{:#?}", line);
|
|
||||||
println!("{:#?}", error);
|
|
||||||
panic!();
|
|
||||||
}
|
|
||||||
//let a_card = a_card.unwrap();
|
|
||||||
//println!("{:?}", a_card.promo_types)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore]
|
|
||||||
fn deserialize_whole_file() {
|
|
||||||
let mut f = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
||||||
f.push("test_files/all-cards.json");
|
|
||||||
assert!(f.exists(), "You need to download the all-cards-... file from Scryfall bulk data. Can be found here: https://scryfall.com/docs/api/bulk-data and rename to all-cards.json");
|
|
||||||
|
|
||||||
let ac = fs::read_to_string(f).unwrap();
|
|
||||||
let _ac: Vec<ScryfallCard> = serde_json::from_str(&ac).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
134
scryfall_deser/src/main.rs
Normal file
134
scryfall_deser/src/main.rs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use scryfall_deser::find_matching_cards_scryfall_style;
|
||||||
|
use scryfall_deser::get_all_mtg_words;
|
||||||
|
use scryfall_deser::get_card_by_name;
|
||||||
|
use scryfall_deser::get_local_cache_folder;
|
||||||
|
use scryfall_deser::init_db;
|
||||||
|
use scryfall_deser::update_db_with_file;
|
||||||
|
use scryfall_deser::GetNameType;
|
||||||
|
use std::process::ExitCode;
|
||||||
|
use std::process::Termination;
|
||||||
|
use textdistance::str::damerau_levenshtein;
|
||||||
|
|
||||||
|
impl Termination for MtgCardExit {
|
||||||
|
fn report(self) -> ExitCode {
|
||||||
|
match self {
|
||||||
|
MtgCardExit::Success => ExitCode::SUCCESS,
|
||||||
|
MtgCardExit::EmptySearchString => ExitCode::from(101),
|
||||||
|
MtgCardExit::NoExactMatchCard => ExitCode::from(102),
|
||||||
|
MtgCardExit::DidYouMean => ExitCode::from(105),
|
||||||
|
MtgCardExit::ExactCardFound => ExitCode::from(200),
|
||||||
|
MtgCardExit::UpdateSuccess => ExitCode::from(201),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MtgCardExit {
|
||||||
|
Success,
|
||||||
|
UpdateSuccess,
|
||||||
|
ExactCardFound,
|
||||||
|
EmptySearchString,
|
||||||
|
NoExactMatchCard,
|
||||||
|
DidYouMean,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
/// Update the local db from given Scryfall bulk download
|
||||||
|
#[arg(short, long)]
|
||||||
|
update: Option<String>,
|
||||||
|
/// Search for the exact string
|
||||||
|
#[arg(short, long)]
|
||||||
|
exact: bool,
|
||||||
|
/// Text to search for card with
|
||||||
|
search_text: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exact_search(search_strings: Vec<String>) -> MtgCardExit {
|
||||||
|
let search_string = search_strings.join(" ");
|
||||||
|
let card = get_card_by_name(&search_string, GetNameType::Name);
|
||||||
|
match card {
|
||||||
|
None => {
|
||||||
|
println!("No card found with exact name of {}", search_string);
|
||||||
|
MtgCardExit::NoExactMatchCard
|
||||||
|
}
|
||||||
|
Some(c) => {
|
||||||
|
println!("{}", c);
|
||||||
|
MtgCardExit::ExactCardFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For use with find_matching_cards
|
||||||
|
fn _combine_search_strings(search_strings: Vec<String>) -> String {
|
||||||
|
let mut search_string = String::new();
|
||||||
|
for card in search_strings {
|
||||||
|
search_string.push_str(&card.to_lowercase());
|
||||||
|
search_string.push_str(" ");
|
||||||
|
}
|
||||||
|
search_string.pop();
|
||||||
|
search_string
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> MtgCardExit {
|
||||||
|
let args = Args::parse();
|
||||||
|
if let Some(update) = args.update {
|
||||||
|
dbg!(&update);
|
||||||
|
init_db();
|
||||||
|
let mut path = get_local_cache_folder();
|
||||||
|
path.push(update);
|
||||||
|
// FIXME - if you pass a bad file or something, it just deletes the db
|
||||||
|
update_db_with_file(path);
|
||||||
|
return MtgCardExit::UpdateSuccess;
|
||||||
|
}
|
||||||
|
if args.search_text.is_empty() {
|
||||||
|
dbg!("You need to put some card text to search");
|
||||||
|
return MtgCardExit::EmptySearchString;
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.exact {
|
||||||
|
let res = exact_search(args.search_text);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut matching_cards = find_matching_cards_scryfall_style(&args.search_text);
|
||||||
|
dbg!(&args.search_text);
|
||||||
|
dbg!(&matching_cards);
|
||||||
|
|
||||||
|
if matching_cards.is_empty() {
|
||||||
|
let mtg_words = get_all_mtg_words();
|
||||||
|
let mut close_names = Vec::new();
|
||||||
|
for search_string in args.search_text {
|
||||||
|
for mtg_card_name in &mtg_words {
|
||||||
|
let dist = damerau_levenshtein(&search_string, &mtg_card_name);
|
||||||
|
if dist <= 2 {
|
||||||
|
close_names.push((dist, mtg_card_name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close_names.sort_by_key(|k| k.0);
|
||||||
|
for (_, card) in close_names {
|
||||||
|
println!("{}", card);
|
||||||
|
}
|
||||||
|
return MtgCardExit::DidYouMean;
|
||||||
|
} else if matching_cards.len() == 1 {
|
||||||
|
// FIXME - theres a bug in here - try searching Nalf
|
||||||
|
let card = get_card_by_name(&matching_cards[0].name, GetNameType::Name).unwrap();
|
||||||
|
println!("{}", card);
|
||||||
|
// TODO update this to be more meaningful
|
||||||
|
return MtgCardExit::ExactCardFound;
|
||||||
|
} else {
|
||||||
|
matching_cards.sort();
|
||||||
|
for card in matching_cards {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
get_card_by_name(&card.lowercase_name, GetNameType::LowercaseName)
|
||||||
|
.unwrap()
|
||||||
|
.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// TODO update this to be more meaningful
|
||||||
|
return MtgCardExit::Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
80
scryfall_deser/src/utils.rs
Normal file
80
scryfall_deser/src/utils.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
use dir_spec::Dir;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub const LOCAL_FOLDER: &str = "scryfall";
|
||||||
|
pub const SQLITE_FILENAME: &str = "scryfall_db.sqlite3";
|
||||||
|
|
||||||
|
pub fn get_local_data_folder() -> PathBuf {
|
||||||
|
let data_folder = Dir::data_home();
|
||||||
|
match data_folder {
|
||||||
|
None => {
|
||||||
|
panic!("Can't find a data folder - really don't know what the problem is sorry");
|
||||||
|
}
|
||||||
|
Some(mut f) => {
|
||||||
|
f.push(LOCAL_FOLDER);
|
||||||
|
f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_local_cache_folder() -> PathBuf {
|
||||||
|
let cache_folder = Dir::cache_home();
|
||||||
|
match cache_folder {
|
||||||
|
None => {
|
||||||
|
panic!("Can't find a cache folder - really don't know what the problem is sorry");
|
||||||
|
}
|
||||||
|
Some(mut f) => {
|
||||||
|
f.push(LOCAL_FOLDER);
|
||||||
|
f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_local_data_sqlite_file() -> PathBuf {
|
||||||
|
let mut folder = get_local_data_folder();
|
||||||
|
folder.push(SQLITE_FILENAME);
|
||||||
|
folder
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: this should be idempotent - creating a dir always is... right?
|
||||||
|
pub fn create_data_folder() {
|
||||||
|
let f = get_local_data_folder();
|
||||||
|
let ret = fs::create_dir(&f);
|
||||||
|
match ret {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => {
|
||||||
|
if e.raw_os_error().unwrap() == 17 {
|
||||||
|
// 17 = this is folder already exists - which is fine for us
|
||||||
|
// TODO probably should use e.kind() for better readability
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
panic!(
|
||||||
|
"Couldn't create folder within your cache folder: {}. Error is {}",
|
||||||
|
f.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: this should be idempotent - creating a dir always is... right?
|
||||||
|
pub fn create_cache_folder() {
|
||||||
|
let f = get_local_cache_folder();
|
||||||
|
let ret = fs::create_dir(&f);
|
||||||
|
match ret {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => {
|
||||||
|
if e.raw_os_error().unwrap() == 17 {
|
||||||
|
// 17 = this is folder already exists - which is fine for us
|
||||||
|
// TODO probably should use e.kind() for better readability
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
panic!(
|
||||||
|
"Couldn't create folder within your cache folder: {}. Error is {}",
|
||||||
|
f.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,32 @@
|
|||||||
{
|
{
|
||||||
"object": "card",
|
"object": "card",
|
||||||
"id": "f5d24a5b-c950-4fd9-99e6-a4b979d915b3",
|
"id": "bd8fa327-dd41-4737-8f19-2cf5eb1f7cdd",
|
||||||
"oracle_id": "5089ec1a-f881-4d55-af14-5d996171203b",
|
"oracle_id": "5089ec1a-f881-4d55-af14-5d996171203b",
|
||||||
"multiverse_ids": [],
|
"multiverse_ids": [
|
||||||
|
382866
|
||||||
|
],
|
||||||
|
"mtgo_id": 53155,
|
||||||
|
"mtgo_foil_id": 53156,
|
||||||
"name": "Black Lotus",
|
"name": "Black Lotus",
|
||||||
"lang": "en",
|
"lang": "en",
|
||||||
"released_at": "2023-12-08",
|
"released_at": "2014-06-16",
|
||||||
"uri": "https://api.scryfall.com/cards/f5d24a5b-c950-4fd9-99e6-a4b979d915b3",
|
"uri": "https://api.scryfall.com/cards/bd8fa327-dd41-4737-8f19-2cf5eb1f7cdd",
|
||||||
"scryfall_uri": "https://scryfall.com/card/ovnt/2023NA/black-lotus?utm_source=api",
|
"scryfall_uri": "https://scryfall.com/card/vma/4/black-lotus?utm_source=api",
|
||||||
"layout": "normal",
|
"layout": "normal",
|
||||||
"highres_image": false,
|
"highres_image": true,
|
||||||
"image_status": "lowres",
|
"image_status": "highres_scan",
|
||||||
"image_uris": {
|
"image_uris": {
|
||||||
"small": "https://cards.scryfall.io/small/front/f/5/f5d24a5b-c950-4fd9-99e6-a4b979d915b3.jpg?1738105453",
|
"small": "https://cards.scryfall.io/small/front/b/d/bd8fa327-dd41-4737-8f19-2cf5eb1f7cdd.jpg?1614638838",
|
||||||
"normal": "https://cards.scryfall.io/normal/front/f/5/f5d24a5b-c950-4fd9-99e6-a4b979d915b3.jpg?1738105453",
|
"normal": "https://cards.scryfall.io/normal/front/b/d/bd8fa327-dd41-4737-8f19-2cf5eb1f7cdd.jpg?1614638838",
|
||||||
"large": "https://cards.scryfall.io/large/front/f/5/f5d24a5b-c950-4fd9-99e6-a4b979d915b3.jpg?1738105453",
|
"large": "https://cards.scryfall.io/large/front/b/d/bd8fa327-dd41-4737-8f19-2cf5eb1f7cdd.jpg?1614638838",
|
||||||
"png": "https://cards.scryfall.io/png/front/f/5/f5d24a5b-c950-4fd9-99e6-a4b979d915b3.png?1738105453",
|
"png": "https://cards.scryfall.io/png/front/b/d/bd8fa327-dd41-4737-8f19-2cf5eb1f7cdd.png?1614638838",
|
||||||
"art_crop": "https://cards.scryfall.io/art_crop/front/f/5/f5d24a5b-c950-4fd9-99e6-a4b979d915b3.jpg?1738105453",
|
"art_crop": "https://cards.scryfall.io/art_crop/front/b/d/bd8fa327-dd41-4737-8f19-2cf5eb1f7cdd.jpg?1614638838",
|
||||||
"border_crop": "https://cards.scryfall.io/border_crop/front/f/5/f5d24a5b-c950-4fd9-99e6-a4b979d915b3.jpg?1738105453"
|
"border_crop": "https://cards.scryfall.io/border_crop/front/b/d/bd8fa327-dd41-4737-8f19-2cf5eb1f7cdd.jpg?1614638838"
|
||||||
},
|
},
|
||||||
"mana_cost": "{0}",
|
"mana_cost": "{0}",
|
||||||
"cmc": 0,
|
"cmc": 0,
|
||||||
"type_line": "Artifact",
|
"type_line": "Artifact",
|
||||||
"oracle_text": "{T}, Sacrifice Black Lotus: Add three mana of any one color.",
|
"oracle_text": "{T}, Sacrifice this artifact: Add three mana of any one color.",
|
||||||
"colors": [],
|
"colors": [],
|
||||||
"color_identity": [],
|
"color_identity": [],
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
@@ -40,7 +44,6 @@
|
|||||||
"timeless": "not_legal",
|
"timeless": "not_legal",
|
||||||
"gladiator": "not_legal",
|
"gladiator": "not_legal",
|
||||||
"pioneer": "not_legal",
|
"pioneer": "not_legal",
|
||||||
"explorer": "not_legal",
|
|
||||||
"modern": "not_legal",
|
"modern": "not_legal",
|
||||||
"legacy": "banned",
|
"legacy": "banned",
|
||||||
"pauper": "not_legal",
|
"pauper": "not_legal",
|
||||||
@@ -58,43 +61,44 @@
|
|||||||
"predh": "banned"
|
"predh": "banned"
|
||||||
},
|
},
|
||||||
"games": [
|
"games": [
|
||||||
"paper"
|
"mtgo"
|
||||||
],
|
],
|
||||||
"reserved": true,
|
"reserved": true,
|
||||||
"foil": false,
|
"game_changer": false,
|
||||||
|
"foil": true,
|
||||||
"nonfoil": true,
|
"nonfoil": true,
|
||||||
"finishes": [
|
"finishes": [
|
||||||
"nonfoil"
|
"nonfoil",
|
||||||
|
"foil"
|
||||||
],
|
],
|
||||||
"oversized": true,
|
"oversized": false,
|
||||||
"promo": false,
|
"promo": false,
|
||||||
"reprint": true,
|
"reprint": true,
|
||||||
"variation": false,
|
"variation": false,
|
||||||
"set_id": "c6a6b61b-143a-43f2-b74d-b140f3d93490",
|
"set_id": "a944551a-73fa-41cd-9159-e8d0e4674403",
|
||||||
"set": "ovnt",
|
"set": "vma",
|
||||||
"set_name": "Vintage Championship",
|
"set_name": "Vintage Masters",
|
||||||
"set_type": "memorabilia",
|
"set_type": "masters",
|
||||||
"set_uri": "https://api.scryfall.com/sets/c6a6b61b-143a-43f2-b74d-b140f3d93490",
|
"set_uri": "https://api.scryfall.com/sets/a944551a-73fa-41cd-9159-e8d0e4674403",
|
||||||
"set_search_uri": "https://api.scryfall.com/cards/search?order=set&q=e%3Aovnt&unique=prints",
|
"set_search_uri": "https://api.scryfall.com/cards/search?order=set&q=e%3Avma&unique=prints",
|
||||||
"scryfall_set_uri": "https://scryfall.com/sets/ovnt?utm_source=api",
|
"scryfall_set_uri": "https://scryfall.com/sets/vma?utm_source=api",
|
||||||
"rulings_uri": "https://api.scryfall.com/cards/f5d24a5b-c950-4fd9-99e6-a4b979d915b3/rulings",
|
"rulings_uri": "https://api.scryfall.com/cards/bd8fa327-dd41-4737-8f19-2cf5eb1f7cdd/rulings",
|
||||||
"prints_search_uri": "https://api.scryfall.com/cards/search?order=released&q=oracleid%3A5089ec1a-f881-4d55-af14-5d996171203b&unique=prints",
|
"prints_search_uri": "https://api.scryfall.com/cards/search?order=released&q=oracleid%3A5089ec1a-f881-4d55-af14-5d996171203b&unique=prints",
|
||||||
"collector_number": "2023NA",
|
"collector_number": "4",
|
||||||
"digital": false,
|
"digital": true,
|
||||||
"rarity": "special",
|
"rarity": "bonus",
|
||||||
"flavor_text": "2023 North America\nVintage Championship",
|
|
||||||
"card_back_id": "0aeebaf5-8c7d-4636-9e82-8c27447861f7",
|
"card_back_id": "0aeebaf5-8c7d-4636-9e82-8c27447861f7",
|
||||||
"artist": "Scott M. Fischer",
|
"artist": "Chris Rahn",
|
||||||
"artist_ids": [
|
"artist_ids": [
|
||||||
"23b0cf43-3e43-44c6-8329-96446eca5bce"
|
"7742047e-0f80-4c0f-a530-d07460165e86"
|
||||||
],
|
],
|
||||||
"illustration_id": "4bc3f69f-66b6-4a6f-8b55-09df0ea4cb89",
|
"illustration_id": "da62ded1-bedd-44c6-8950-ca56e691a899",
|
||||||
"border_color": "black",
|
"border_color": "black",
|
||||||
"frame": "2015",
|
"frame": "2015",
|
||||||
"security_stamp": "oval",
|
"security_stamp": "oval",
|
||||||
"full_art": false,
|
"full_art": false,
|
||||||
"textless": false,
|
"textless": false,
|
||||||
"booster": false,
|
"booster": true,
|
||||||
"story_spotlight": false,
|
"story_spotlight": false,
|
||||||
"prices": {
|
"prices": {
|
||||||
"usd": null,
|
"usd": null,
|
||||||
@@ -102,16 +106,17 @@
|
|||||||
"usd_etched": null,
|
"usd_etched": null,
|
||||||
"eur": null,
|
"eur": null,
|
||||||
"eur_foil": null,
|
"eur_foil": null,
|
||||||
"tix": null
|
"tix": "41.98"
|
||||||
},
|
},
|
||||||
"related_uris": {
|
"related_uris": {
|
||||||
"tcgplayer_infinite_articles": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&trafcat=infinite&u=https%3A%2F%2Finfinite.tcgplayer.com%2Fsearch%3FcontentMode%3Darticle%26game%3Dmagic%26q%3DBlack%2BLotus",
|
"gatherer": "https://gatherer.wizards.com/Pages/Card/Details.aspx?multiverseid=382866&printed=false",
|
||||||
"tcgplayer_infinite_decks": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&trafcat=infinite&u=https%3A%2F%2Finfinite.tcgplayer.com%2Fsearch%3FcontentMode%3Ddeck%26game%3Dmagic%26q%3DBlack%2BLotus",
|
"tcgplayer_infinite_articles": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&trafcat=tcgplayer.com%2Fsearch%2Farticles&u=https%3A%2F%2Fwww.tcgplayer.com%2Fsearch%2Farticles%3FproductLineName%3Dmagic%26q%3DBlack%2BLotus",
|
||||||
|
"tcgplayer_infinite_decks": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&trafcat=tcgplayer.com%2Fsearch%2Fdecks&u=https%3A%2F%2Fwww.tcgplayer.com%2Fsearch%2Fdecks%3FproductLineName%3Dmagic%26q%3DBlack%2BLotus",
|
||||||
"edhrec": "https://edhrec.com/route/?cc=Black+Lotus"
|
"edhrec": "https://edhrec.com/route/?cc=Black+Lotus"
|
||||||
},
|
},
|
||||||
"purchase_uris": {
|
"purchase_uris": {
|
||||||
"tcgplayer": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&u=https%3A%2F%2Fwww.tcgplayer.com%2Fsearch%2Fmagic%2Fproduct%3FproductLineName%3Dmagic%26q%3DBlack%2BLotus%26view%3Dgrid",
|
"tcgplayer": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&u=https%3A%2F%2Fwww.tcgplayer.com%2Fsearch%2Fmagic%2Fproduct%3FproductLineName%3Dmagic%26q%3DBlack%2BLotus%26view%3Dgrid",
|
||||||
"cardmarket": "https://www.cardmarket.com/en/Magic/Products/Search?referrer=scryfall&searchString=Black+Lotus&utm_campaign=card_prices&utm_medium=text&utm_source=scryfall",
|
"cardmarket": "https://www.cardmarket.com/en/Magic/Products/Search?referrer=scryfall&searchString=Black+Lotus&utm_campaign=card_prices&utm_medium=text&utm_source=scryfall",
|
||||||
"cardhoarder": "https://www.cardhoarder.com/cards?affiliate_id=scryfall&data%5Bsearch%5D=Black+Lotus&ref=card-profile&utm_campaign=affiliate&utm_medium=card&utm_source=scryfall"
|
"cardhoarder": "https://www.cardhoarder.com/cards/53155?affiliate_id=scryfall&ref=card-profile&utm_campaign=affiliate&utm_medium=card&utm_source=scryfall"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,6 @@
|
|||||||
"timeless": "not_legal",
|
"timeless": "not_legal",
|
||||||
"gladiator": "not_legal",
|
"gladiator": "not_legal",
|
||||||
"pioneer": "not_legal",
|
"pioneer": "not_legal",
|
||||||
"explorer": "not_legal",
|
|
||||||
"modern": "not_legal",
|
"modern": "not_legal",
|
||||||
"legacy": "not_legal",
|
"legacy": "not_legal",
|
||||||
"pauper": "not_legal",
|
"pauper": "not_legal",
|
||||||
@@ -64,6 +63,7 @@
|
|||||||
"paper"
|
"paper"
|
||||||
],
|
],
|
||||||
"reserved": false,
|
"reserved": false,
|
||||||
|
"game_changer": false,
|
||||||
"foil": true,
|
"foil": true,
|
||||||
"nonfoil": true,
|
"nonfoil": true,
|
||||||
"finishes": [
|
"finishes": [
|
||||||
@@ -100,23 +100,22 @@
|
|||||||
"booster": true,
|
"booster": true,
|
||||||
"story_spotlight": false,
|
"story_spotlight": false,
|
||||||
"prices": {
|
"prices": {
|
||||||
"usd": "0.29",
|
"usd": "0.31",
|
||||||
"usd_foil": "16.25",
|
"usd_foil": "15.64",
|
||||||
"usd_etched": null,
|
"usd_etched": null,
|
||||||
"eur": "0.29",
|
"eur": "0.19",
|
||||||
"eur_foil": "12.69",
|
"eur_foil": "11.09",
|
||||||
"tix": null
|
"tix": null
|
||||||
},
|
},
|
||||||
"related_uris": {
|
"related_uris": {
|
||||||
"gatherer": "https://gatherer.wizards.com/Pages/Card/Details.aspx?multiverseid=74257&printed=false",
|
"gatherer": "https://gatherer.wizards.com/Pages/Card/Details.aspx?multiverseid=74257&printed=false",
|
||||||
"tcgplayer_infinite_articles": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&trafcat=infinite&u=https%3A%2F%2Finfinite.tcgplayer.com%2Fsearch%3FcontentMode%3Darticle%26game%3Dmagic%26q%3DLittle%2BGirl",
|
"tcgplayer_infinite_articles": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&trafcat=tcgplayer.com%2Fsearch%2Farticles&u=https%3A%2F%2Fwww.tcgplayer.com%2Fsearch%2Farticles%3FproductLineName%3Dmagic%26q%3DLittle%2BGirl",
|
||||||
"tcgplayer_infinite_decks": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&trafcat=infinite&u=https%3A%2F%2Finfinite.tcgplayer.com%2Fsearch%3FcontentMode%3Ddeck%26game%3Dmagic%26q%3DLittle%2BGirl",
|
"tcgplayer_infinite_decks": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&trafcat=tcgplayer.com%2Fsearch%2Fdecks&u=https%3A%2F%2Fwww.tcgplayer.com%2Fsearch%2Fdecks%3FproductLineName%3Dmagic%26q%3DLittle%2BGirl",
|
||||||
"edhrec": "https://edhrec.com/route/?cc=Little+Girl"
|
"edhrec": "https://edhrec.com/route/?cc=Little+Girl"
|
||||||
},
|
},
|
||||||
"purchase_uris": {
|
"purchase_uris": {
|
||||||
"tcgplayer": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&u=https%3A%2F%2Fwww.tcgplayer.com%2Fproduct%2F37883%3Fpage%3D1",
|
"tcgplayer": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&u=https%3A%2F%2Fwww.tcgplayer.com%2Fproduct%2F37883%3Fpage%3D1",
|
||||||
"cardmarket": "https://www.cardmarket.com/en/Magic/Products/Singles/Unhinged/Little-Girl?referrer=scryfall&utm_campaign=card_prices&utm_medium=text&utm_source=scryfall",
|
"cardmarket": "https://www.cardmarket.com/en/Magic/Products?idProduct=14779&referrer=scryfall&utm_campaign=card_prices&utm_medium=text&utm_source=scryfall",
|
||||||
"cardhoarder": "https://www.cardhoarder.com/cards?affiliate_id=scryfall&data%5Bsearch%5D=Little+Girl&ref=card-profile&utm_campaign=affiliate&utm_medium=card&utm_source=scryfall"
|
"cardhoarder": "https://www.cardhoarder.com/cards?affiliate_id=scryfall&data%5Bsearch%5D=Little+Girl&ref=card-profile&utm_campaign=affiliate&utm_medium=card&utm_source=scryfall"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
{
|
{
|
||||||
"object": "card",
|
"object": "card",
|
||||||
"id": "7b3f0e26-7784-452d-acc8-9f7181e0f7d5",
|
"id": "248c76d3-b5cb-4582-be17-7cd1d0cb0f58",
|
||||||
"oracle_id": "c1fc5923-c3cd-448a-98d1-c154661c2812",
|
"oracle_id": "c1fc5923-c3cd-448a-98d1-c154661c2812",
|
||||||
"multiverse_ids": [
|
"multiverse_ids": [
|
||||||
615951
|
615415
|
||||||
],
|
],
|
||||||
"mtgo_id": 109062,
|
"mtgo_id": 109166,
|
||||||
"tcgplayer_id": 495791,
|
"arena_id": 85078,
|
||||||
"cardmarket_id": 710527,
|
"tcgplayer_id": 495610,
|
||||||
|
"cardmarket_id": 710196,
|
||||||
"name": "Nissa, Resurgent Animist",
|
"name": "Nissa, Resurgent Animist",
|
||||||
"lang": "en",
|
"lang": "en",
|
||||||
"released_at": "2023-05-12",
|
"released_at": "2023-05-12",
|
||||||
"uri": "https://api.scryfall.com/cards/7b3f0e26-7784-452d-acc8-9f7181e0f7d5",
|
"uri": "https://api.scryfall.com/cards/248c76d3-b5cb-4582-be17-7cd1d0cb0f58",
|
||||||
"scryfall_uri": "https://scryfall.com/card/mat/162/nissa-resurgent-animist?utm_source=api",
|
"scryfall_uri": "https://scryfall.com/card/mat/22/nissa-resurgent-animist?utm_source=api",
|
||||||
"layout": "normal",
|
"layout": "normal",
|
||||||
"highres_image": true,
|
"highres_image": true,
|
||||||
"image_status": "highres_scan",
|
"image_status": "highres_scan",
|
||||||
"image_uris": {
|
"image_uris": {
|
||||||
"small": "https://cards.scryfall.io/small/front/7/b/7b3f0e26-7784-452d-acc8-9f7181e0f7d5.jpg?1684341884",
|
"small": "https://cards.scryfall.io/small/front/2/4/248c76d3-b5cb-4582-be17-7cd1d0cb0f58.jpg?1684340632",
|
||||||
"normal": "https://cards.scryfall.io/normal/front/7/b/7b3f0e26-7784-452d-acc8-9f7181e0f7d5.jpg?1684341884",
|
"normal": "https://cards.scryfall.io/normal/front/2/4/248c76d3-b5cb-4582-be17-7cd1d0cb0f58.jpg?1684340632",
|
||||||
"large": "https://cards.scryfall.io/large/front/7/b/7b3f0e26-7784-452d-acc8-9f7181e0f7d5.jpg?1684341884",
|
"large": "https://cards.scryfall.io/large/front/2/4/248c76d3-b5cb-4582-be17-7cd1d0cb0f58.jpg?1684340632",
|
||||||
"png": "https://cards.scryfall.io/png/front/7/b/7b3f0e26-7784-452d-acc8-9f7181e0f7d5.png?1684341884",
|
"png": "https://cards.scryfall.io/png/front/2/4/248c76d3-b5cb-4582-be17-7cd1d0cb0f58.png?1684340632",
|
||||||
"art_crop": "https://cards.scryfall.io/art_crop/front/7/b/7b3f0e26-7784-452d-acc8-9f7181e0f7d5.jpg?1684341884",
|
"art_crop": "https://cards.scryfall.io/art_crop/front/2/4/248c76d3-b5cb-4582-be17-7cd1d0cb0f58.jpg?1684340632",
|
||||||
"border_crop": "https://cards.scryfall.io/border_crop/front/7/b/7b3f0e26-7784-452d-acc8-9f7181e0f7d5.jpg?1684341884"
|
"border_crop": "https://cards.scryfall.io/border_crop/front/2/4/248c76d3-b5cb-4582-be17-7cd1d0cb0f58.jpg?1684340632"
|
||||||
},
|
},
|
||||||
"mana_cost": "{2}{G}",
|
"mana_cost": "{2}{G}",
|
||||||
"cmc": 3,
|
"cmc": 3,
|
||||||
@@ -47,13 +48,12 @@
|
|||||||
"W"
|
"W"
|
||||||
],
|
],
|
||||||
"legalities": {
|
"legalities": {
|
||||||
"standard": "legal",
|
"standard": "not_legal",
|
||||||
"future": "legal",
|
"future": "not_legal",
|
||||||
"historic": "legal",
|
"historic": "legal",
|
||||||
"timeless": "legal",
|
"timeless": "legal",
|
||||||
"gladiator": "legal",
|
"gladiator": "legal",
|
||||||
"pioneer": "legal",
|
"pioneer": "legal",
|
||||||
"explorer": "legal",
|
|
||||||
"modern": "legal",
|
"modern": "legal",
|
||||||
"legacy": "legal",
|
"legacy": "legal",
|
||||||
"pauper": "not_legal",
|
"pauper": "not_legal",
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
"penny": "not_legal",
|
"penny": "not_legal",
|
||||||
"commander": "legal",
|
"commander": "legal",
|
||||||
"oathbreaker": "legal",
|
"oathbreaker": "legal",
|
||||||
"standardbrawl": "legal",
|
"standardbrawl": "not_legal",
|
||||||
"brawl": "legal",
|
"brawl": "legal",
|
||||||
"alchemy": "not_legal",
|
"alchemy": "not_legal",
|
||||||
"paupercommander": "not_legal",
|
"paupercommander": "not_legal",
|
||||||
@@ -76,6 +76,7 @@
|
|||||||
"mtgo"
|
"mtgo"
|
||||||
],
|
],
|
||||||
"reserved": false,
|
"reserved": false,
|
||||||
|
"game_changer": false,
|
||||||
"foil": true,
|
"foil": true,
|
||||||
"nonfoil": true,
|
"nonfoil": true,
|
||||||
"finishes": [
|
"finishes": [
|
||||||
@@ -93,9 +94,9 @@
|
|||||||
"set_uri": "https://api.scryfall.com/sets/6727e43d-31b6-45b0-ae05-7a811ba72f70",
|
"set_uri": "https://api.scryfall.com/sets/6727e43d-31b6-45b0-ae05-7a811ba72f70",
|
||||||
"set_search_uri": "https://api.scryfall.com/cards/search?order=set&q=e%3Amat&unique=prints",
|
"set_search_uri": "https://api.scryfall.com/cards/search?order=set&q=e%3Amat&unique=prints",
|
||||||
"scryfall_set_uri": "https://scryfall.com/sets/mat?utm_source=api",
|
"scryfall_set_uri": "https://scryfall.com/sets/mat?utm_source=api",
|
||||||
"rulings_uri": "https://api.scryfall.com/cards/7b3f0e26-7784-452d-acc8-9f7181e0f7d5/rulings",
|
"rulings_uri": "https://api.scryfall.com/cards/248c76d3-b5cb-4582-be17-7cd1d0cb0f58/rulings",
|
||||||
"prints_search_uri": "https://api.scryfall.com/cards/search?order=released&q=oracleid%3Ac1fc5923-c3cd-448a-98d1-c154661c2812&unique=prints",
|
"prints_search_uri": "https://api.scryfall.com/cards/search?order=released&q=oracleid%3Ac1fc5923-c3cd-448a-98d1-c154661c2812&unique=prints",
|
||||||
"collector_number": "162",
|
"collector_number": "22",
|
||||||
"digital": false,
|
"digital": false,
|
||||||
"rarity": "mythic",
|
"rarity": "mythic",
|
||||||
"watermark": "desparked",
|
"watermark": "desparked",
|
||||||
@@ -108,35 +109,31 @@
|
|||||||
"border_color": "black",
|
"border_color": "black",
|
||||||
"frame": "2015",
|
"frame": "2015",
|
||||||
"frame_effects": [
|
"frame_effects": [
|
||||||
"legendary",
|
"legendary"
|
||||||
"extendedart"
|
|
||||||
],
|
],
|
||||||
"security_stamp": "oval",
|
"security_stamp": "oval",
|
||||||
"full_art": false,
|
"full_art": false,
|
||||||
"textless": false,
|
"textless": false,
|
||||||
"booster": false,
|
"booster": true,
|
||||||
"story_spotlight": false,
|
"story_spotlight": false,
|
||||||
"promo_types": [
|
"edhrec_rank": 2064,
|
||||||
"boosterfun"
|
|
||||||
],
|
|
||||||
"edhrec_rank": 2163,
|
|
||||||
"prices": {
|
"prices": {
|
||||||
"usd": "24.96",
|
"usd": "16.18",
|
||||||
"usd_foil": "32.13",
|
"usd_foil": "16.73",
|
||||||
"usd_etched": null,
|
"usd_etched": null,
|
||||||
"eur": "27.94",
|
"eur": "22.93",
|
||||||
"eur_foil": "35.15",
|
"eur_foil": "22.72",
|
||||||
"tix": "0.99"
|
"tix": "0.17"
|
||||||
},
|
},
|
||||||
"related_uris": {
|
"related_uris": {
|
||||||
"gatherer": "https://gatherer.wizards.com/Pages/Card/Details.aspx?multiverseid=615951&printed=false",
|
"gatherer": "https://gatherer.wizards.com/Pages/Card/Details.aspx?multiverseid=615415&printed=false",
|
||||||
"tcgplayer_infinite_articles": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&trafcat=infinite&u=https%3A%2F%2Finfinite.tcgplayer.com%2Fsearch%3FcontentMode%3Darticle%26game%3Dmagic%26q%3DNissa%252C%2BResurgent%2BAnimist",
|
"tcgplayer_infinite_articles": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&trafcat=tcgplayer.com%2Fsearch%2Farticles&u=https%3A%2F%2Fwww.tcgplayer.com%2Fsearch%2Farticles%3FproductLineName%3Dmagic%26q%3DNissa%252C%2BResurgent%2BAnimist",
|
||||||
"tcgplayer_infinite_decks": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&trafcat=infinite&u=https%3A%2F%2Finfinite.tcgplayer.com%2Fsearch%3FcontentMode%3Ddeck%26game%3Dmagic%26q%3DNissa%252C%2BResurgent%2BAnimist",
|
"tcgplayer_infinite_decks": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&trafcat=tcgplayer.com%2Fsearch%2Fdecks&u=https%3A%2F%2Fwww.tcgplayer.com%2Fsearch%2Fdecks%3FproductLineName%3Dmagic%26q%3DNissa%252C%2BResurgent%2BAnimist",
|
||||||
"edhrec": "https://edhrec.com/route/?cc=Nissa%2C+Resurgent+Animist"
|
"edhrec": "https://edhrec.com/route/?cc=Nissa%2C+Resurgent+Animist"
|
||||||
},
|
},
|
||||||
"purchase_uris": {
|
"purchase_uris": {
|
||||||
"tcgplayer": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&u=https%3A%2F%2Fwww.tcgplayer.com%2Fproduct%2F495791%3Fpage%3D1",
|
"tcgplayer": "https://partner.tcgplayer.com/c/4931599/1830156/21018?subId1=api&u=https%3A%2F%2Fwww.tcgplayer.com%2Fproduct%2F495610%3Fpage%3D1",
|
||||||
"cardmarket": "https://www.cardmarket.com/en/Magic/Products/Singles/MoM-TA-E/Nissa-Resurgent-Animist-V3?referrer=scryfall&utm_campaign=card_prices&utm_medium=text&utm_source=scryfall",
|
"cardmarket": "https://www.cardmarket.com/en/Magic/Products?idProduct=710196&referrer=scryfall&utm_campaign=card_prices&utm_medium=text&utm_source=scryfall",
|
||||||
"cardhoarder": "https://www.cardhoarder.com/cards/109062?affiliate_id=scryfall&ref=card-profile&utm_campaign=affiliate&utm_medium=card&utm_source=scryfall"
|
"cardhoarder": "https://www.cardhoarder.com/cards/109166?affiliate_id=scryfall&ref=card-profile&utm_campaign=affiliate&utm_medium=card&utm_source=scryfall"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user