From d0f49fce07d6a7a891053d1d5170ea50ee5231a1 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Sat, 23 Nov 2019 16:20:58 +0100 Subject: [PATCH] Refactor cube API, add buy/pool info --- cmd/buy.ts | 41 ++++++++++ cmd/cube-dev.ts | 157 ++----------------------------------- cmd/cube.ts | 85 ++------------------ cmd/pool.ts | 54 +++++++++++++ lib/api.ts | 54 ++++++++++--- lib/cube.ts | 189 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 4 +- templates/buy.ejs | 10 +++ templates/cube.ejs | 11 ++- 9 files changed, 359 insertions(+), 246 deletions(-) create mode 100644 cmd/buy.ts create mode 100644 cmd/pool.ts create mode 100644 lib/cube.ts create mode 100644 templates/buy.ejs diff --git a/cmd/buy.ts b/cmd/buy.ts new file mode 100644 index 0000000..af8bcfb --- /dev/null +++ b/cmd/buy.ts @@ -0,0 +1,41 @@ +import * as ejs from "ejs"; +import { existsSync } from "fs"; + +import { Article, asyncLoadJSON, CardMarketApi, MCMDB } from "../lib"; +import { cubeCards, genCube } from "../lib/cube"; + +async function run() { + if (process.argv.length < 3) { + console.error("Usage: yarn buy "); + process.exit(1); + return; + } + const uid = process.argv[2]; + const uidCards = `${uid}-cards.json`; + if (!existsSync("mcmCards.json")) { + console.error("Card db is missing! Run 'yarn convert-db' first."); + process.exit(1); + } + if (!existsSync(uidCards)) { + console.error(`Could not find ${uidCards}! Run 'yarn fetch ${uid}' first.`); + process.exit(1); + } + let db = await asyncLoadJSON("mcmCards.json"); + let articles = await asyncLoadJSON(`${uid}-cards.json`); + + const cards = cubeCards(await genCube(db, articles)); + + let api = new CardMarketApi(); + + // Can only list 100 articles per request + let offset = 0; + while (offset < cards.length) { + const xml: string = await ejs.renderFile("templates/buy.ejs", { + cards: cards.slice(offset, offset + 100) + }); + await api.put("/shoppingcart", null, xml); + offset += 100; + } +} + +run(); diff --git a/cmd/cube-dev.ts b/cmd/cube-dev.ts index 3865e58..cdbf827 100644 --- a/cmd/cube-dev.ts +++ b/cmd/cube-dev.ts @@ -1,126 +1,12 @@ -import * as ejs from "ejs"; import { existsSync } from "fs"; import { createServer } from "http"; -import { - Article, - asyncLoadJSON, - CardItem, - dictMap, - filterDict, - leanArticle, - leanCard, - MCMDB, - onlyUnique, - spanBy, -} from "../lib"; - -const colorNames = { - CL: "Colorless", - MC: "Multicolor", - L: "Land", - W: "White", - U: "Blue", - B: "Black", - R: "Red", - G: "Green", - WU: "Azorius", - UB: "Dimir", - BR: "Rakdos", - RG: "Gruul", - WG: "Selesnya", - WB: "Orzhov", - UR: "Izzet", - BG: "Golgari", - WR: "Boros", - UG: "Simic", - WUG: "Bant", - WUB: "Esper", - UBR: "Grixis", - BRG: "Jund", - WRG: "Naya", - WBG: "Abzan", - WUR: "Jeskai", - UBG: "Sultai", - WBR: "Mardu", - URG: "Temur" -}; - -const columns: Record boolean> = { - W: c => c.types[0] != "Land" && colorid(c.colorIdentity) == "W", - U: c => c.types[0] != "Land" && colorid(c.colorIdentity) == "U", - B: c => c.types[0] != "Land" && colorid(c.colorIdentity) == "B", - R: c => c.types[0] != "Land" && colorid(c.colorIdentity) == "R", - G: c => c.types[0] != "Land" && colorid(c.colorIdentity) == "G", - MC: c => c.types[0] != "Land" && c.colorIdentity.length > 1, - CL: c => c.types[0] != "Land" && colorid(c.colorIdentity) == "CL", - L: c => c.types[0] == "Land" -}; - -function wubrg(a: string, b: string) { - const order = ["W", "U", "B", "R", "G"]; - const indexA = order.indexOf(a); - const indexB = order.indexOf(b); - return indexA - indexB; -} - -function colorid(colors: string[]): string { - if (colors.length < 1) { - return "CL"; - } - return colors.sort(wubrg).join(""); -} - -function prettyColor(color: string) { - if (color in colorNames) { - return colorNames[color]; - } - return color; -} - -const allTypes = [ - "Creature", - "Planeswalker", - "Instant", - "Sorcery", - "Artifact", - "Enchantment" -]; -function typeSort(a: string, b: string): number { - const indexA = allTypes.indexOf(a); - const indexB = allTypes.indexOf(b); - return indexA - indexB; -} - -function abcSort(a: string, b: string): number { - if (a > b) { - return 1; - } - if (b > a) { - return -1; - } - return 0; -} - -// This would be properly typed but as of currently TS does not allow circular references for types -function deepSum(dict: any): number { - if (Array.isArray(dict)) { - return dict.length; - } - let total = 0; - for (let key in dict) { - if (Array.isArray(dict[key])) { - total += dict[key].length; - } else { - total += deepSum(dict[key]); - } - } - return total; -} +import { Article, asyncLoadJSON, MCMDB } from "../lib"; +import { cubeHTML, genCube } from "../lib/cube"; async function run() { if (process.argv.length < 3) { - console.error("Usage: yarn fetch "); + console.error("Usage: yarn cube-dev "); process.exit(1); return; } @@ -137,43 +23,10 @@ async function run() { let db = await asyncLoadJSON("mcmCards.json"); let articles = await asyncLoadJSON(`${uid}-cards.json`); - let cards: CardItem[] = articles - .filter(art => art.idProduct in db) - .map(art => { - const card = db[art.idProduct]; - return { - ...leanArticle(art), - ...leanCard(card) - }; - }); + const cubecards = await genCube(db, articles); - let valid = cards - .filter( - c => - (c.language == "Italian" || c.language == "English") && - c.price <= 0.1 && - (c.rarity == "rare" || c.rarity == "mythic") - ) - .filter(onlyUnique); - - // Filter by colors and cmc (nested) - let colorcards = filterDict(valid, columns); - let cmccards = dictMap(colorcards, (cards, col) => - dictMap( - spanBy(cards, c => - col == "MC" || col == "L" ? colorid(c.colorIdentity) : c.types[0] - ), - typed => spanBy(typed, c => c.convertedManaCost) - ) - ); const server = createServer(async (req, res) => { - const template = await ejs.renderFile("templates/cube.ejs", { - user: uid, - cards: valid, - cmccards, - columns, - utils: { wubrg, prettyColor, colorid, deepSum, typeSort, abcSort } - }); + const template = await cubeHTML(uid, cubecards); res.end(template); }); diff --git a/cmd/cube.ts b/cmd/cube.ts index 8b1c8c3..065e590 100644 --- a/cmd/cube.ts +++ b/cmd/cube.ts @@ -1,64 +1,11 @@ -import * as ejs from "ejs"; import { existsSync, writeFile } from "fs"; -import { Article, asyncLoadJSON, CardItem, leanArticle, leanCard, MCMDB, onlyUnique } from "../lib"; - -const colorNames = { - CL: "Colorless", - W: "White", - U: "Blue", - B: "Black", - R: "Red", - G: "Green", - WU: "Azorius", - UB: "Dimir", - BR: "Rakdos", - RG: "Gruul", - WG: "Selesnya", - WB: "Orzhov", - UR: "Izzet", - BG: "Golgari", - WR: "Boros", - UG: "Simic", - WUG: "Bant", - WUB: "Esper", - UBR: "Grixis", - BRG: "Jund", - WRG: "Naya", - WBG: "Abzan", - WUR: "Jeskai", - UBG: "Sultai", - WBR: "Mardu", - URG: "Temur" -}; - -const columns: Record boolean> = { - W: c => colorid(c.colorIdentity) == "W", - U: c => colorid(c.colorIdentity) == "U", - B: c => colorid(c.colorIdentity) == "B", - R: c => colorid(c.colorIdentity) == "R", - G: c => colorid(c.colorIdentity) == "G", - MC: c => c.colorIdentity.length > 0, - CL: c => colorid(c.colorIdentity) == "CL" -}; - -function wubrg(a: string, b: string) { - const order = ["W", "U", "B", "R", "G"]; - const indexA = order.indexOf(a); - const indexB = order.indexOf(b); - return indexA - indexB; -} - -function colorid(colors: string[]): string { - if (colors.length < 1) { - return "CL"; - } - return colors.sort(wubrg).join(""); -} +import { Article, asyncLoadJSON, MCMDB } from "../lib"; +import { cubeHTML, genCube } from "../lib/cube"; async function run() { if (process.argv.length < 3) { - console.error("Usage: yarn fetch "); + console.error("Usage: yarn cube "); process.exit(1); return; } @@ -75,31 +22,9 @@ async function run() { let db = await asyncLoadJSON("mcmCards.json"); let articles = await asyncLoadJSON(`${uid}-cards.json`); - let cards: CardItem[] = articles - .filter(art => art.idProduct in db) - .map(art => { - const card = db[art.idProduct]; - return { - ...leanArticle(art), - ...leanCard(card) - }; - }); + const cubecards = await genCube(db, articles); + const template = await cubeHTML(uid, cubecards); - let valid = cards - .filter( - c => - (c.language == "Italian" || c.language == "English") && - c.price <= 0.1 && - (c.rarity == "rare" || c.rarity == "mythic") - ) - .filter(onlyUnique); - - const template = await ejs.renderFile("templates/cube.ejs", { - user: uid, - cards: valid, - columns, - utils: { wubrg, colorNames, colorid } - }); let cubeFile = `${uid}-cube.html`; writeFile(cubeFile, template, {}, err => { if (err) { diff --git a/cmd/pool.ts b/cmd/pool.ts new file mode 100644 index 0000000..8e3f91b --- /dev/null +++ b/cmd/pool.ts @@ -0,0 +1,54 @@ +import { existsSync } from "fs"; + +import { Article, asyncLoadJSON, CardItem, MCMDB } from "../lib"; +import { cubeCards, genCube } from "../lib/cube"; + +async function run() { + if (process.argv.length < 3) { + console.error("Usage: yarn buy "); + process.exit(1); + return; + } + const uid = process.argv[2]; + const uidCards = `${uid}-cards.json`; + if (!existsSync("mcmCards.json")) { + console.error("Card db is missing! Run 'yarn convert-db' first."); + process.exit(1); + } + if (!existsSync(uidCards)) { + console.error(`Could not find ${uidCards}! Run 'yarn fetch ${uid}' first.`); + process.exit(1); + } + let db = await asyncLoadJSON("mcmCards.json"); + let articles = await asyncLoadJSON(`${uid}-cards.json`); + + const cards = cubeCards(await genCube(db, articles)); + + let types: Record = {}; + cards.forEach(c => { + c.subtypes.forEach(t => { + if (t in types) { + types[t].push(c); + } else { + types[t] = [c]; + } + }); + }); + + console.log("TRIBAL? Top 10 tribes:"); + const entries = Object.entries(types) + .sort((a, b) => b[1].length - a[1].length) + .slice(0, 10) + .forEach(([typ, typcards]) => { + console.log( + ` ${typ.padEnd(16, " ")}${typcards.length + .toString() + .padStart(3)} cards (${( + (typcards.length / cards.length) * + 100 + ).toFixed(1)}%)` + ); + }); +} + +run(); diff --git a/lib/api.ts b/lib/api.ts index ca752b2..b7e011c 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -1,5 +1,5 @@ import { stringify } from "querystring"; -import { get } from "request"; +import * as request from "request"; import { Article } from "./types"; @@ -18,28 +18,39 @@ export class CardMarketApi { this.AccessSecret = process.env.ACCESS_SECRET; } - async get(path: string, params?: Record): Promise { - const oauthParameters = { + async request( + method: string, + path: string, + params: Record | undefined, + options: any + ): Promise { + const oauth = { realm: `${URI}${path}`, consumer_key: this.AppToken, consumer_secret: this.AppSecret, token: this.AccessToken, token_secret: this.AccessSecret }; + const uri = oauth.realm + (params ? `?${stringify(params)}` : ""); return new Promise((resolve, reject) => { - get( - oauthParameters.realm + (params ? `?${stringify(params)}` : ""), + request( { - oauth: oauthParameters + method, + uri, + oauth, + ...options }, (error, response) => { if (error) { - throw error; + reject(error); } else if (response.statusCode > 299) { - throw JSON.stringify({ - statusCode: response.statusCode, - statusMessage: response.statusMessage - }); + reject( + JSON.stringify({ + statusCode: response.statusCode, + statusMessage: response.statusMessage, + statusBody: response.body + }) + ); } try { resolve(JSON.parse(response.body)); @@ -51,6 +62,27 @@ export class CardMarketApi { }); } + async get(path: string, params?: Record): Promise { + return await this.request("GET", path, params, {}); + } + + async put( + path: string, + params?: Record, + data?: string + ): Promise { + return await this.request( + "PUT", + path, + params, + data + ? { + body: data + } + : {} + ); + } + async getAllArticles(uid: string): Promise { const perPage = 1000; let start = 0; diff --git a/lib/cube.ts b/lib/cube.ts new file mode 100644 index 0000000..0f5faa7 --- /dev/null +++ b/lib/cube.ts @@ -0,0 +1,189 @@ +import * as ejs from "ejs"; + +import { Article, CardItem, dictMap, filterDict, leanArticle, leanCard, MCMDB, onlyUnique, spanBy } from "."; + +const colorNames = { + CL: "Colorless", + MC: "Multicolor", + L: "Land", + W: "White", + U: "Blue", + B: "Black", + R: "Red", + G: "Green", + WU: "Azorius", + UB: "Dimir", + BR: "Rakdos", + RG: "Gruul", + WG: "Selesnya", + WB: "Orzhov", + UR: "Izzet", + BG: "Golgari", + WR: "Boros", + UG: "Simic", + WUG: "Bant", + WUB: "Esper", + UBR: "Grixis", + BRG: "Jund", + WRG: "Naya", + WBG: "Abzan", + WUR: "Jeskai", + UBG: "Sultai", + WBR: "Mardu", + URG: "Temur" +}; + +const columns: Record boolean> = { + W: c => colorid(c.colorIdentity) == "W", + U: c => colorid(c.colorIdentity) == "U", + B: c => colorid(c.colorIdentity) == "B", + R: c => colorid(c.colorIdentity) == "R", + G: c => colorid(c.colorIdentity) == "G", + MC: c => c.colorIdentity.length > 1, + CL: c => colorid(c.colorIdentity) == "CL" +}; + +function wubrg(a: string, b: string) { + const order = ["W", "U", "B", "R", "G"]; + const indexA = order.indexOf(a); + const indexB = order.indexOf(b); + return indexA - indexB; +} + +function colorid(colors: string[]): string { + if (colors.length < 1) { + return "CL"; + } + return colors.sort(wubrg).join(""); +} + +function prettyColor(color: string) { + if (color in colorNames) { + return colorNames[color]; + } + return color; +} + +const allTypes = [ + "Creature", + "Planeswalker", + "Instant", + "Sorcery", + "Artifact", + "Enchantment" +]; +function typeSort(a: string, b: string): number { + const indexA = allTypes.indexOf(a); + const indexB = allTypes.indexOf(b); + return indexA - indexB; +} + +function abcSort(a: string, b: string): number { + if (a > b) { + return 1; + } + if (b > a) { + return -1; + } + return 0; +} + +// This would be properly typed but as of currently TS does not allow circular references for types +function deepSum(dict: any): number { + if (Array.isArray(dict)) { + return dict.length; + } + let total = 0; + for (let key in dict) { + if (Array.isArray(dict[key])) { + total += dict[key].length; + } else { + total += deepSum(dict[key]); + } + } + return total; +} + +export function cubeCards(c: CubeCards): CardItem[] { + let cards: CardItem[] = []; + dictMap(c, bytyp => + dictMap(bytyp, bycmc => + dictMap(bycmc, cardlist => (cards = cards.concat(cardlist))) + ) + ); + return cards; +} + +function byRank(a: CardItem, b: CardItem): number { + return a.edhrecRank - b.edhrecRank; +} + +export type ByCMC = Record; +export type ByType = Record; +export type ByColor = Record; +export type CubeCards = ByColor>>; + +export async function genCube( + db: MCMDB, + articles: Article[] +): Promise { + const cards: CardItem[] = articles + .filter(art => art.idProduct in db) + .map(art => { + const card = db[art.idProduct]; + return { + ...leanArticle(art), + ...leanCard(card) + }; + }); + + const valid = cards + .filter( + c => + (c.language == "Italian" || c.language == "English") && + c.price < 0.2 && + (c.rarity == "rare" || c.rarity == "mythic") + ) + .filter(onlyUnique); + + const categorized = filterDict(valid, columns); + + // Cut cards + const filtered = dictMap(categorized, (pool, col) => { + switch (col) { + case "MC": + const colors = spanBy(pool, c => colorid(c.colorIdentity)); + let cards = []; + for (const color in colors) { + cards = cards.concat(colors[color].sort(byRank).slice(0, 3)); + } + return cards; + case "CL": + return pool.sort(byRank).slice(0, 30); + default: + return pool.sort(byRank).slice(0, 58); + } + }); + + // Filter by colors and cmc (nested) + return dictMap(filtered, (cards, col) => + dictMap( + spanBy(cards, c => + col == "MC" || col == "L" + ? colorid(c.colorIdentity) + : c.types.slice(-1)[0] + ), + typed => spanBy(typed, c => c.convertedManaCost) + ) + ); +} + +export async function cubeHTML(user, cubecards: CubeCards) { + return await ejs.renderFile("templates/cube.ejs", { + user, + cards: cubeCards(cubecards), + cubecards, + columns, + utils: { wubrg, prettyColor, colorid, deepSum, typeSort, abcSort } + }); +} diff --git a/package.json b/package.json index a837b96..3d17284 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "build-db": "ts-node cmd/convert.ts", "find": "ts-node cmd/find.ts", "cube": "ts-node cmd/cube.ts", - "cube-watch": "ts-node cmd/cube-dev.ts" + "cube-watch": "ts-node cmd/cube-dev.ts", + "buy": "ts-node cmd/buy.ts", + "pool-info": "ts-node cmd/pool.ts" }, "dependencies": { "@types/ejs": "^2.6.3", diff --git a/templates/buy.ejs b/templates/buy.ejs new file mode 100644 index 0000000..72b02a7 --- /dev/null +++ b/templates/buy.ejs @@ -0,0 +1,10 @@ + + + add + <% cards.forEach(card => { %> +
+ <%= card.idArticle %> + 1 +
+ <% }); %> +
\ No newline at end of file diff --git a/templates/cube.ejs b/templates/cube.ejs index 1187b1a..97069b4 100644 --- a/templates/cube.ejs +++ b/templates/cube.ejs @@ -2,6 +2,7 @@ + Junk rare cube @@ -144,9 +150,10 @@

Junk rare cube (<%=cards.length%> cards)

Using cards from <%= user %>

+

Est. price without shipping: € <%= cards.reduce((a, c) => a + c.price, 0).toFixed(2) %>

- <% Object.entries(cmccards).forEach(([color, colorcards]) => { %> + <% Object.entries(cubecards).forEach(([color, colorcards]) => { %>

<%=utils.prettyColor(color)%> (<%=utils.deepSum(colorcards)%>)

@@ -163,7 +170,7 @@
  • - <%= card.name %> + <%= card.name %>€ <%=card.price.toFixed(2)%> - #<%=card.edhrecRank%>
  • <% }); %>