Yu-Gi-Oh! Deck Building and Card Inventory Management web interface written in Common Lisp, utilizing HTMX.
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

478 Zeilen
19KB

  1. #|
  2. src/models/ygoprodeck.json.lisp
  3. Version 3 JSON Importer
  4. TODO Write a test suite - it should be easier now that it's in FNF
  5. The idea behind this code is you'll be able to one click button download and update the db.
  6. (cl-deck-builder2.models.ygoprodeck.json2::json-import-cardinfo #P"/tmp/cardinfo.json")
  7. |#
  8. (in-package #:cl-user)
  9. (in-package #:cl-deck-builder2.models.ygoprodeck.json)
  10. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  11. (defvar *json* nil
  12. "The currently loaded CARDINFO data pulled from YGOProDeck API.")
  13. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  14. (defmethod cardinfo-import ((obj cardinfo) &optional force)
  15. "Set *JSON* to the decoded JSON data from CARDINFO, by processing it into CARDINFO-JSON using CARDINFO-CONVERT.
  16. Optionally, FORCE the data to be loaded anyway.
  17. If the (CARDINFO-OUTPUT OBJ) does not exist, try to (CARDINFO-INPUT *CARDINFO*)."
  18. (cardinfo-load obj force)
  19. (process-all-nodes))
  20. (defmethod cardinfo-load ((obj cardinfo) &optional force)
  21. (let ((output (cardinfo-output obj)))
  22. (v:info :ygoprodeck.json "CARDINFO-LOAD ~a (exists:~a) (force:~a)" output (cardinfo-output-exists-p obj) force)
  23. (if (cardinfo-output-exists-p obj)
  24. (progn
  25. (setf *json* (with-open-file (f output)
  26. (cl-json:decode-json f)))))))
  27. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  28. (defun json-cardinfo-base-fields (plist)
  29. "Filter only the base fields for this CARD-INFO PLIST."
  30. (set-difference plist
  31. '((:banlist--info)
  32. (:card--images)
  33. (:card--prices)
  34. (:card--sets)
  35. (:linkmarkers)
  36. (:misc--info))
  37. :key #'car))
  38. ;; (defparameter +ygo-card-ids+ (make-hash-table :test #'equal))
  39. ;; (defparameter +ygo-card-descs+ (make-hash-table :test #'equal))
  40. ;; TODO put all of these hash tables inside another hash table, then
  41. ;; make an API around it. I've seen that done before with CLOS, maybe
  42. ;; in cl-opengl defvao stuff. Remember?
  43. (defparameter +ygo-card-names+ (make-hash-table :test #'equal))
  44. (defparameter +ygo-card-types+ (make-hash-table :test #'equal))
  45. (defparameter +ygo-card-races+ (make-hash-table :test #'equal))
  46. (defparameter +ygo-card-frame-types+ (make-hash-table :test #'equal))
  47. (defparameter +ygo-card-archetypes+ (make-hash-table :test #'equal))
  48. (defparameter +ygo-card-attributes+ (make-hash-table :test #'equal))
  49. (defparameter +ygo-set-names+ (make-hash-table :test #'equal))
  50. (defparameter +ygo-set-codes+ (make-hash-table :test #'equal))
  51. (defparameter +ygo-set-rarity+ (make-hash-table :test #'equal))
  52. (defparameter +ygo-set-editions+ (make-hash-table :test #'equal))
  53. ;; Here we use the ID/PASSCODE so EQ is Okay
  54. (defparameter +ygo-card-prices+ (make-hash-table)) ;; One to one: setf place
  55. (defparameter +ygo-alternative-artwork+ (make-hash-table)) ;; Many to many: push place
  56. (defparameter +ygo-cards+ (make-hash-table)) ;; One to one: setf place
  57. (defparameter +ygo-sets+ (make-hash-table)) ;; Many to many: push place
  58. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  59. ;; Field Names
  60. (defparameter +ygo-banlist-info+ (make-hash-table))
  61. (defparameter +ygo-format-info+ (make-hash-table))
  62. (defparameter +ygo-linkmarker-info+ (make-hash-table))
  63. (defparameter +ygo-banlist-names+ (make-hash-table :test #'equal))
  64. (defparameter +ygo-format-names+ (make-hash-table :test #'equal))
  65. (defparameter +ygo-linkmarker-names+ (make-hash-table :test #'equal))
  66. (defparameter +ygo-set-rarity-codes+ (make-hash-table :test #'equal))
  67. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  68. ;; The bulk of the operation ... FIND-OR-MAKE-INSTANCE + SYNC-HASH-TABLE
  69. (defun sync-hash-table (ht)
  70. (let ((values (alexandria:hash-table-values ht)))
  71. (cond ((null values) nil)
  72. (;; The ht is a bunch of lists (card sets, prices, images)
  73. (listp (car values))
  74. (dolist (lst values)
  75. (unless (mito:dao-synced (car lst))
  76. (do-grouped-insert lst))))
  77. (t
  78. (unless (mito:dao-synced (car values))
  79. (do-grouped-insert values))))))
  80. ;; TODO Where should this go? I'm seeing bits of code like this all over.
  81. (defun %keywordify (s)
  82. (alexandria:make-keyword
  83. (string-upcase
  84. (substitute #\- #\Space s))))
  85. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  86. ;; Importer v3
  87. (defun extract-json-tag (json tag)
  88. "Get all the tags from JSON matching TAG."
  89. (mapcar (lambda (alist) (assoc-utils:aget alist tag)) json))
  90. (defun extract-concat (json tag)
  91. "Concatenate the results of EXTRACT-JSON-TAG into a single list for MAPCAR'ing."
  92. (apply #'concatenate 'list (extract-json-tag json tag)))
  93. (defun extract-card-sets (json)
  94. "Just extract :CARD--SETS. No Processing"
  95. (extract-concat json :card--sets))
  96. (defun extract-card-set-map (json)
  97. "Extract PASSCODE => SETS mapping."
  98. (let ((lst '()))
  99. (dolist (node json (copy-alist
  100. (apply #'concatenate 'list (reverse lst))))
  101. (let* ((node-id (assoc-utils:aget node :id))
  102. (maybe-card-sets
  103. (mapcar (lambda (alist)
  104. (acons :passcode-id node-id alist))
  105. (assoc-utils:aget node :card--sets))))
  106. (when maybe-card-sets
  107. (push maybe-card-sets lst))))))
  108. (defun extract-card-images (json)
  109. "Just extract :CARD--IMAGES. No Processing"
  110. (extract-concat json :card--images))
  111. (defun extract-card-alternative-images-map (json)
  112. "Mapping of ID to alternative art IDs."
  113. (let ((lst '()))
  114. (dolist (node json (reverse lst))
  115. (let* ((alternate-id (assoc-utils:aget node :id))
  116. (maybe-alternative-images
  117. (remove alternate-id
  118. (mapcar (lambda (alist)
  119. (assoc-utils:aget alist :id))
  120. (assoc-utils:aget node :card--images)))))
  121. (when maybe-alternative-images
  122. (mapcar (lambda (node-id)
  123. (push (list :passcode-id node-id
  124. :alternate-id alternate-id)
  125. lst))
  126. maybe-alternative-images))))))
  127. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  128. ;; Working with the base fields
  129. (defun extract-card-tag (json tag)
  130. "Extract TAG from JSON using EXTRACT-JSON-TAG. Also, SORT, and SUBSTITUTE NIL for \"\"."
  131. (sort
  132. (substitute "" NIL
  133. (remove-duplicates
  134. (extract-json-tag json tag)
  135. :test #'string=))
  136. #'string<))
  137. (defun extract-card-name (json)
  138. (extract-card-tag json :name))
  139. (defun extract-card-type (json)
  140. (extract-card-tag json :type))
  141. (defun extract-card-race (json)
  142. (extract-card-tag json :race))
  143. (defun extract-card-frame-type (json)
  144. (extract-card-tag json :frame-type))
  145. (defun extract-card-archetype (json)
  146. (extract-card-tag json :archetype))
  147. (defun extract-card-attribute (json)
  148. (extract-card-tag json :attribute))
  149. (defun extract-cards (json)
  150. "Just extract the base field for every card."
  151. (mapcar #'json-cardinfo-base-fields json))
  152. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  153. (defun extract-card-banlist-info (json)
  154. "Lots of cards don't have banlist info records."
  155. (let ((lst '()))
  156. (dolist (node json (copy-list lst))
  157. (let ((node-id (assoc-utils:aget node :id))
  158. (maybe-banlist-info
  159. (assoc-utils:alist-plist
  160. (assoc-utils:aget node :banlist--info))))
  161. (when maybe-banlist-info
  162. (push (append (list :passcode-id node-id)
  163. maybe-banlist-info)
  164. lst))))))
  165. (defun extract-card-banlist-names (json)
  166. "Just extract the names of the ban status. \"Banned\" etc."
  167. (let ((lst '()))
  168. (dolist (node json (copy-list lst))
  169. (let ((maybe-banlist-names
  170. (assoc-utils:alist-values
  171. (assoc-utils:aget node :banlist--info))))
  172. (when maybe-banlist-names
  173. (loop for name in maybe-banlist-names do
  174. (pushnew name lst :test #'string=)))))))
  175. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  176. (defun extract-card-prices (json)
  177. (extract-json-tag json :card--prices))
  178. (defun extract-card-price-map (json)
  179. "Extract :CARD--PRICE and process into ID => PRICES mapping"
  180. (let ((lst '()))
  181. (dolist (node json lst)
  182. (let ((node-id (assoc-utils:aget node :id))
  183. (maybe-price-data
  184. (assoc-utils:alist-plist
  185. (apply #'concatenate 'list
  186. (assoc-utils:aget node :card--prices)))))
  187. (when maybe-price-data
  188. (push (append (list :passcode-id node-id)
  189. maybe-price-data)
  190. lst))))))
  191. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  192. (defun extract-card-misc-info (json)
  193. (extract-json-tag json :misc--info))
  194. (defun extract-card-misc-info-map (json)
  195. "Extract :MISC--INFO and process into ID => FORMATS mapping"
  196. (let ((lst '()))
  197. (dolist (node json lst)
  198. (let* ((node-id (assoc-utils:aget node :id))
  199. (maybe-misc-info
  200. (assoc-utils:alist-plist
  201. (remove :formats
  202. (apply #'concatenate 'list
  203. (assoc-utils:aget node :misc--info))
  204. :key #'car))))
  205. (when maybe-misc-info
  206. (push (append (list :passcode-id node-id)
  207. maybe-misc-info)
  208. lst))))))
  209. ;; TODO
  210. (defun extract-card-format-info (json)
  211. "Extract :MISC--INFO and process into ID => FORMATS mapping"
  212. (let ((lst '()))
  213. (dolist (node json lst)
  214. (let* ((node-id (assoc-utils:aget node :id))
  215. (maybe-misc-info
  216. (assoc-utils:aget
  217. (apply #'concatenate 'list
  218. (assoc-utils:aget node :misc--info))
  219. :formats)))
  220. (when maybe-misc-info
  221. (push (list node-id maybe-misc-info) lst))))))
  222. ;; TODO - The card formats need to be processed like the card-sets and cards
  223. (defun process-card-format-info-map (json)
  224. (let ((info (extract-card-format-info json)))
  225. (with-connection (db)
  226. (with-transaction
  227. (mapcar (lambda (row)
  228. (list :passcode-id (car row)
  229. (apply #'concatenate 'list
  230. (mapcar (lambda (tag)
  231. (let ((db-tag (or (find-or-create-instance 'ygo-format-name :name tag))))
  232. (list (%keywordify tag) db-tag)))
  233. (cadr row)))))
  234. info)))))
  235. (defun extract-card-format-names (json)
  236. (sort
  237. (remove-duplicates
  238. (apply #'concatenate 'list
  239. (mapcar (lambda (alist) (assoc-utils:aget alist :formats))
  240. (apply #'concatenate 'list
  241. (extract-card-misc-info json))))
  242. :test #'string=)
  243. #'string<))
  244. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  245. (defun extract-card-linkmarker-names (json)
  246. (sort (remove-duplicates
  247. (extract-concat json :linkmarkers)
  248. :test #'string=)
  249. #'string<))
  250. ;; TODO
  251. (defun extract-card-linkmarker-info (json)
  252. "Just extract :CARD--IMAGES. No Processing"
  253. (let ((lst '()))
  254. (dolist (node json lst)
  255. (let ((node-id (assoc-utils:aget node :id))
  256. (maybe-linkmarkers
  257. (assoc-utils:aget node :linkmarkers)))
  258. (when maybe-linkmarkers
  259. (push (list node-id maybe-linkmarkers) lst))))))
  260. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  261. ;; Working with CARD--SETS
  262. ;; Here, JSON shuld be: (extract-card-sets json)
  263. (defun extract-set-tag (json tag)
  264. (sort (remove-duplicates
  265. (extract-json-tag json tag)
  266. :test #'string=)
  267. #'string<))
  268. (defun extract-set-name (json)
  269. (extract-set-tag json :set--name))
  270. (defun extract-set-code (json)
  271. (extract-set-tag json :set--code))
  272. (defun extract-set-rarity (json)
  273. (extract-set-tag json :set--rarity))
  274. (defun extract-set-rarity-code (json)
  275. (extract-set-tag json :set--rarity--code))
  276. (defun extract-set-edition (json)
  277. (extract-set-tag json :set--edition))
  278. (defun extract-set-price (json)
  279. (extract-set-tag json :set--price))
  280. (defun extract-set-url (json)
  281. (extract-set-tag json :set--url))
  282. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  283. ;; Card Images
  284. (defun extract-card-main-image (json)
  285. "From :CARD--IMAGES, extract the first IMAGE--URL. The other two are small and cropped images."
  286. (sort
  287. (mapcar (lambda (alist) (assoc-utils:aget alist :image--url))
  288. (extract-concat json :card--images))
  289. #'string<))
  290. (defun gen-hash-table (ht class tags)
  291. "Just a list mapping like \"Normal\" => 1, \"Tested\" => 2, and so on. Could be used for anything that has a NAME column."
  292. (v:info :ygoprodeck.json "GEN-HASH-TABLE: ~a" class)
  293. (with-connection (db)
  294. (with-transaction
  295. (dolist (tag tags ht)
  296. (unless (gethash tag ht)
  297. (setf (gethash tag ht)
  298. (or (mito:find-dao class :name tag)
  299. (mito:create-dao class :name tag))))))))
  300. (defun gen-hash-mapping-table (class tags &key (test 'equal))
  301. "A more complex mapping of PASSCODE -> DATA"
  302. (let ((ht (make-hash-table :test test)))
  303. (with-connection (db)
  304. (with-transaction
  305. (dolist (node tags ht)
  306. (let ((passcode-id (getf node :passcode-id)))
  307. (setf (gethash passcode-id ht)
  308. (or (mito:find-dao class :passcode-id passcode-id)
  309. (apply #'mito:create-dao class node)))))))))
  310. (defun gen-hash-map-list-table (class tags &key (test 'equal))
  311. "A more complex mapping of PASSCODE -> (DATA, DATA, DATA)"
  312. (let ((ht (make-hash-table :test test)))
  313. (with-connection (db)
  314. (with-transaction
  315. (dolist (node tags ht)
  316. (let ((passcode-id (getf node :passcode-id)))
  317. (push (or (apply #'mito:find-dao class node)
  318. (apply #'mito:create-dao class node))
  319. (gethash passcode-id ht))))))))
  320. (defun process-card-nodes (json)
  321. "Mostly preserved from V2."
  322. (let* ((base-fields (json-cardinfo-base-fields json))
  323. (id (assoc-utils:aget base-fields :id))
  324. (args (list
  325. :name (assoc-utils:aget base-fields :name)
  326. :desc (assoc-utils:aget base-fields :desc)
  327. :archetype (gethash (assoc-utils:aget base-fields :archetype) +ygo-card-archetypes+)
  328. :attribute (gethash (assoc-utils:aget base-fields :attribute) +ygo-card-attributes+)
  329. :frame-type (gethash (assoc-utils:aget base-fields :frame-type) +ygo-card-frame-types+)
  330. :race (gethash (assoc-utils:aget base-fields :race) +ygo-card-races+)
  331. :card-type (gethash (assoc-utils:aget base-fields :type) +ygo-card-types+)
  332. :atk (assoc-utils:aget base-fields :atk)
  333. :def (assoc-utils:aget base-fields :def)
  334. :scale (assoc-utils:aget base-fields :scale)
  335. :level (assoc-utils:aget base-fields :level)))
  336. (found (or (mito:find-dao 'ygo-card :id id)
  337. (apply #'create-dao 'ygo-card :id id args))))
  338. (unless (gethash id +ygo-cards+)
  339. (setf (gethash id +ygo-cards+) found))))
  340. (defun process-card-sets (&rest sets)
  341. "Mostly preserved from V2."
  342. (dolist (node sets +ygo-sets+)
  343. (let* ((args
  344. (list :passcode-id (assoc-utils:aget node :passcode-id)
  345. :code (gethash (assoc-utils:aget node :set--code) +ygo-set-codes+)
  346. :name (gethash (assoc-utils:aget node :set--name) +ygo-set-names+)
  347. :edition (gethash (assoc-utils:aget node :set--edition) +ygo-set-editions+)
  348. :rarity (gethash (assoc-utils:aget node :set--rarity) +ygo-set-rarity+)
  349. :rarity-code (gethash (assoc-utils:aget node :set--rarity--code) +ygo-set-rarity-codes+)))
  350. (rest-args
  351. (list :price (assoc-utils:aget node :set--price)
  352. :url (assoc-utils:aget node :set--url)))
  353. (found (or (apply #'mito:find-dao 'ygo-set args)
  354. (apply #'mito:create-dao 'ygo-set (append args rest-args)))))
  355. (pushnew found
  356. (gethash (assoc-utils:aget node :passcode-id) +ygo-sets+)
  357. :test #'mito:object=))))
  358. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  359. ;; TODO for Import-v4 - It might be useful to have PASSCODE as the ID,
  360. ;; that way, there is a uniform interface for querying card info...
  361. ;;
  362. ;; TODO Split this up?
  363. (defun pre-process-nodes (json)
  364. (let ((card-names (extract-card-name json))
  365. (card-types (extract-card-type json))
  366. (card-races (extract-card-race json))
  367. (card-frame-types (extract-card-frame-type json))
  368. (card-archetypes (extract-card-archetype json))
  369. (card-attributes (extract-card-attribute json))
  370. (card-sets (extract-card-sets json)))
  371. (gen-hash-table +ygo-card-names+ 'ygo-card-name card-names)
  372. (gen-hash-table +ygo-card-archetypes+ 'ygo-card-archetype card-archetypes)
  373. (gen-hash-table +ygo-card-attributes+ 'ygo-card-attribute card-attributes)
  374. (gen-hash-table +ygo-card-frame-types+ 'ygo-card-frame-type card-frame-types)
  375. (gen-hash-table +ygo-card-races+ 'ygo-card-race card-races)
  376. (gen-hash-table +ygo-card-types+ 'ygo-card-type card-types)
  377. (gen-hash-table +ygo-set-names+ 'ygo-set-name (extract-set-name card-sets))
  378. (gen-hash-table +ygo-set-codes+ 'ygo-set-code (extract-set-code card-sets))
  379. (gen-hash-table +ygo-set-rarity+ 'ygo-set-rarity (extract-set-rarity card-sets))
  380. (gen-hash-table +ygo-set-editions+ 'ygo-set-edition (extract-set-edition card-sets))
  381. (gen-hash-table +ygo-set-rarity-codes+ 'ygo-set-rarity-code (extract-set-rarity-code card-sets))
  382. (gen-hash-table +ygo-banlist-names+ 'ygo-banlist-name (extract-card-banlist-info card-sets))
  383. (gen-hash-table +ygo-linkmarker-names+ 'ygo-linkmarker-name (extract-card-linkmarker-names card-sets))
  384. (gen-hash-table +ygo-format-names+ 'ygo-format-name (extract-card-format-names json))))
  385. (defun pre-process-card-prices (json)
  386. (setf +ygo-card-prices+ (gen-hash-mapping-table 'ygo-price (extract-card-price-map json) :test 'eq)))
  387. (defun pre-process-card-alternative-artwork (json)
  388. (setf +ygo-alternative-artwork+ (gen-hash-map-list-table 'ygo-alternative-artwork (extract-card-alternative-images-map json) :test 'eq)))
  389. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  390. (defun process-all-nodes (&optional (json *json*))
  391. "Process all nodes from JSON."
  392. (v:info :ygoprodeck.json "PROCESS-ALL-NODES: PRE-PROCESS-NODES")
  393. (pre-process-nodes json)
  394. (pre-process-card-prices json)
  395. (pre-process-card-alternative-artwork json)
  396. ;; +ygo-cards+
  397. (v:info :ygoprodeck.json "PROCESS-ALL-NODES: PROCESS-CARD-NODES")
  398. (with-connection (db)
  399. (with-transaction
  400. (mapcan #'process-card-nodes json)))
  401. ;; +ygo-sets+
  402. (v:info :ygoprodeck.json "PROCESS-ALL-NODES: PROCESS-CARD-SETS")
  403. (with-connection (db)
  404. (with-transaction
  405. (apply #'process-card-sets (extract-card-set-map json)))))