@nekosuneprojects/gaming-infos 2.1.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/sync-api-cache.yml +73 -0
- package/CACHE_SYNC.md +202 -0
- package/CHANGELOG.md +54 -2
- package/README.md +304 -136
- package/data/.cache-manifest.json +112 -0
- package/data/arknightsendfield/characters/akekuri.json +75 -0
- package/data/arknightsendfield/characters/alesh.json +75 -0
- package/data/arknightsendfield/characters/antal.json +75 -0
- package/data/arknightsendfield/characters/arcane.json +75 -0
- package/data/arknightsendfield/characters/arclight.json +75 -0
- package/data/arknightsendfield/characters/ardelia.json +75 -0
- package/data/arknightsendfield/characters/avywenna.json +76 -0
- package/data/arknightsendfield/characters/camille.json +77 -0
- package/data/arknightsendfield/characters/catcher.json +76 -0
- package/data/arknightsendfield/characters/chenqianyu.json +76 -0
- package/data/arknightsendfield/characters/dapan.json +76 -0
- package/data/arknightsendfield/characters/ember.json +76 -0
- package/data/arknightsendfield/characters/endministrator.json +63 -0
- package/data/arknightsendfield/characters/estella.json +76 -0
- package/data/arknightsendfield/characters/fluorite.json +75 -0
- package/data/arknightsendfield/characters/gilberta.json +76 -0
- package/data/arknightsendfield/characters/laevatain.json +76 -0
- package/data/arknightsendfield/characters/lastrite.json +76 -0
- package/data/arknightsendfield/characters/lifeng.json +75 -0
- package/data/arknightsendfield/characters/liino.json +76 -0
- package/data/arknightsendfield/characters/mifu.json +75 -0
- package/data/arknightsendfield/characters/perlica.json +76 -0
- package/data/arknightsendfield/characters/pogranichnik.json +75 -0
- package/data/arknightsendfield/characters/purrchena.json +50 -0
- package/data/arknightsendfield/characters/rossi.json +77 -0
- package/data/arknightsendfield/characters/snowshine.json +75 -0
- package/data/arknightsendfield/characters/tangtang.json +76 -0
- package/data/arknightsendfield/characters/typhoeus.json +57 -0
- package/data/arknightsendfield/characters/wulfgard.json +77 -0
- package/data/arknightsendfield/characters/xaihi.json +76 -0
- package/data/arknightsendfield/characters/yvonne.json +76 -0
- package/data/arknightsendfield/characters/zhuangfangyi.json +76 -0
- package/data/arknightsendfield/meta.json +14 -0
- package/data/fortnite/characters/agentjones.json +43 -0
- package/data/fortnite/characters/agentpeely.json +43 -0
- package/data/fortnite/characters/andyfangerson.json +42 -0
- package/data/fortnite/characters/antonia.json +43 -0
- package/data/fortnite/characters/anwar.json +44 -0
- package/data/fortnite/characters/aphrodite.json +43 -0
- package/data/fortnite/characters/apollo.json +36 -0
- package/data/fortnite/characters/arcticassassin.json +43 -0
- package/data/fortnite/characters/artemis.json +36 -0
- package/data/fortnite/characters/battlewoodboulevard.json +43 -0
- package/data/fortnite/characters/beastmode.json +45 -0
- package/data/fortnite/characters/bogstick.json +43 -0
- package/data/fortnite/characters/boxy.json +44 -0
- package/data/fortnite/characters/bravoleader.json +44 -0
- package/data/fortnite/characters/briteraider.json +43 -0
- package/data/fortnite/characters/brutalboxcars.json +46 -0
- package/data/fortnite/characters/bryne.json +44 -0
- package/data/fortnite/characters/buildersbarracks.json +38 -0
- package/data/fortnite/characters/bullshark.json +43 -0
- package/data/fortnite/characters/bumpybay.json +44 -0
- package/data/fortnite/characters/burd.json +44 -0
- package/data/fortnite/characters/burdemart.json +39 -0
- package/data/fortnite/characters/burdtogo.json +42 -0
- package/data/fortnite/characters/busansanctuary.json +39 -0
- package/data/fortnite/characters/calamaricanyon.json +41 -0
- package/data/fortnite/characters/canyoncrossing.json +45 -0
- package/data/fortnite/characters/captainjones.json +40 -0
- package/data/fortnite/characters/centurion.json +44 -0
- package/data/fortnite/characters/cerberus.json +43 -0
- package/data/fortnite/characters/charon.json +43 -0
- package/data/fortnite/characters/chiara.json +43 -0
- package/data/fortnite/characters/clara.json +43 -0
- package/data/fortnite/characters/classifiedcanyon.json +43 -0
- package/data/fortnite/characters/clawsylodge.json +41 -0
- package/data/fortnite/characters/cletuscornhole.json +43 -0
- package/data/fortnite/characters/contractgiller.json +44 -0
- package/data/fortnite/characters/corruptioncorners.json +42 -0
- package/data/fortnite/characters/countessdaraku.json +45 -0
- package/data/fortnite/characters/crackshotscabin.json +40 -0
- package/data/fortnite/characters/creepycamps.json +36 -0
- package/data/fortnite/characters/crimecity.json +47 -0
- package/data/fortnite/characters/dara.json +42 -0
- package/data/fortnite/characters/darkruby.json +43 -0
- package/data/fortnite/characters/demonsdojo.json +46 -0
- package/data/fortnite/characters/diamonddiva.json +43 -0
- package/data/fortnite/characters/donutdistrict.json +43 -0
- package/data/fortnite/characters/drakonsteelrider.json +43 -0
- package/data/fortnite/characters/dummy.json +45 -0
- package/data/fortnite/characters/edwardscissorhands.json +39 -0
- package/data/fortnite/characters/etheria.json +43 -0
- package/data/fortnite/characters/evergreenterrace.json +41 -0
- package/data/fortnite/characters/fishmarket.json +39 -0
- package/data/fortnite/characters/fishstick.json +45 -0
- package/data/fortnite/characters/fishthicc.json +43 -0
- package/data/fortnite/characters/fortressofsolitude.json +37 -0
- package/data/fortnite/characters/foxyfloodgate.json +43 -0
- package/data/fortnite/characters/freakyfields.json +38 -0
- package/data/fortnite/characters/goldengrove.json +41 -0
- package/data/fortnite/characters/graveheart.json +43 -0
- package/data/fortnite/characters/grievouscampsite.json +38 -0
- package/data/fortnite/characters/guardianamara.json +44 -0
- package/data/fortnite/characters/gwenpool.json +41 -0
- package/data/fortnite/characters/hanamuracourtyard.json +27 -0
- package/data/fortnite/characters/heartbreakranger.json +42 -0
- package/data/fortnite/characters/heatwaveharbor.json +41 -0
- package/data/fortnite/characters/heroichope.json +44 -0
- package/data/fortnite/characters/hope.json +43 -0
- package/data/fortnite/characters/hopefulheights.json +46 -0
- package/data/fortnite/characters/humblehills.json +41 -0
- package/data/fortnite/characters/innovatorslone.json +43 -0
- package/data/fortnite/characters/insight.json +43 -0
- package/data/fortnite/characters/ironman.json +40 -0
- package/data/fortnite/characters/jittershock.json +44 -0
- package/data/fortnite/characters/joeycaliente.json +44 -0
- package/data/fortnite/characters/jonithescarlet.json +43 -0
- package/data/fortnite/characters/khabylame.json +40 -0
- package/data/fortnite/characters/khari.json +44 -0
- package/data/fortnite/characters/kitbash.json +44 -0
- package/data/fortnite/characters/kitesflight.json +41 -0
- package/data/fortnite/characters/knightmare.json +43 -0
- package/data/fortnite/characters/laguna.json +43 -0
- package/data/fortnite/characters/lakespringfield.json +35 -0
- package/data/fortnite/characters/lakespringfieldgasstation.json +39 -0
- package/data/fortnite/characters/lanallane.json +42 -0
- package/data/fortnite/characters/lattelanding.json +41 -0
- package/data/fortnite/characters/lavendra.json +44 -0
- package/data/fortnite/characters/leelah.json +45 -0
- package/data/fortnite/characters/liftylodge.json +42 -0
- package/data/fortnite/characters/lonewolflair.json +42 -0
- package/data/fortnite/characters/longshot.json +41 -0
- package/data/fortnite/characters/lostlake.json +44 -0
- package/data/fortnite/characters/loveranger.json +44 -0
- package/data/fortnite/characters/mae.json +43 -0
- package/data/fortnite/characters/magicmosses.json +42 -0
- package/data/fortnite/characters/maskedmeadows.json +48 -0
- package/data/fortnite/characters/mechateamshadow.json +44 -0
- package/data/fortnite/characters/medusa.json +43 -0
- package/data/fortnite/characters/meowscles.json +42 -0
- package/data/fortnite/characters/mephisto.json +41 -0
- package/data/fortnite/characters/metalmouth.json +44 -0
- package/data/fortnite/characters/mincemeat.json +43 -0
- package/data/fortnite/characters/munitionsexpert.json +42 -0
- package/data/fortnite/characters/myna.json +42 -0
- package/data/fortnite/characters/newsanctuary.json +43 -0
- package/data/fortnite/characters/nia.json +42 -0
- package/data/fortnite/characters/nightshiftforest.json +43 -0
- package/data/fortnite/characters/nisha.json +42 -0
- package/data/fortnite/characters/nolanchance.json +43 -0
- package/data/fortnite/characters/noorah.json +44 -0
- package/data/fortnite/characters/odyssey.json +43 -0
- package/data/fortnite/characters/openaironsen.json +40 -0
- package/data/fortnite/characters/oscar.json +42 -0
- package/data/fortnite/characters/outlawoasis.json +40 -0
- package/data/fortnite/characters/outpostenclave.json +27 -0
- package/data/fortnite/characters/oxrhq.json +37 -0
- package/data/fortnite/characters/paintedpalms.json +41 -0
- package/data/fortnite/characters/peabody.json +42 -0
- package/data/fortnite/characters/peelosopherbananocrates.json +43 -0
- package/data/fortnite/characters/peely.json +41 -0
- package/data/fortnite/characters/peelybone.json +43 -0
- package/data/fortnite/characters/persephone.json +42 -0
- package/data/fortnite/characters/phaedra.json +42 -0
- package/data/fortnite/characters/phantommeowscles.json +43 -0
- package/data/fortnite/characters/piperpace.json +43 -0
- package/data/fortnite/characters/poseidon.json +42 -0
- package/data/fortnite/characters/pumpedpower.json +40 -0
- package/data/fortnite/characters/pumpinpipes.json +39 -0
- package/data/fortnite/characters/pumpkinking.json +40 -0
- package/data/fortnite/characters/purradisemeowscles.json +43 -0
- package/data/fortnite/characters/rangersrest.json +36 -0
- package/data/fortnite/characters/rebeloutpost.json +37 -0
- package/data/fortnite/characters/renegadeshadow.json +44 -0
- package/data/fortnite/characters/resistancebase.json +27 -0
- package/data/fortnite/characters/rippedtides.json +43 -0
- package/data/fortnite/characters/rollingblossomsfarm.json +41 -0
- package/data/fortnite/characters/royalebomber.json +38 -0
- package/data/fortnite/characters/ruckus.json +42 -0
- package/data/fortnite/characters/rust.json +41 -0
- package/data/fortnite/characters/sakuracircle.json +36 -0
- package/data/fortnite/characters/sandystrip.json +44 -0
- package/data/fortnite/characters/saskia.json +45 -0
- package/data/fortnite/characters/seaportcity.json +48 -0
- package/data/fortnite/characters/shakensanctuary.json +42 -0
- package/data/fortnite/characters/shimmerdusk.json +41 -0
- package/data/fortnite/characters/shinyshafts.json +38 -0
- package/data/fortnite/characters/shogunssolitude.json +46 -0
- package/data/fortnite/characters/shuri.json +39 -0
- package/data/fortnite/characters/silashesk.json +42 -0
- package/data/fortnite/characters/slurproom.json +40 -0
- package/data/fortnite/characters/snowheart.json +44 -0
- package/data/fortnite/characters/snowstriker.json +43 -0
- package/data/fortnite/characters/solidsnake.json +39 -0
- package/data/fortnite/characters/sonor.json +46 -0
- package/data/fortnite/characters/spartanassassin.json +43 -0
- package/data/fortnite/characters/spirithuntersaeko.json +41 -0
- package/data/fortnite/characters/springfieldnationalparkstore.json +38 -0
- package/data/fortnite/characters/springfieldnuclearpowerplant.json +39 -0
- package/data/fortnite/characters/springfieldslurpworks.json +40 -0
- package/data/fortnite/characters/squibblyshores.json +42 -0
- package/data/fortnite/characters/squibblyspointlighthouse.json +37 -0
- package/data/fortnite/characters/starroakley.json +45 -0
- package/data/fortnite/characters/steelsight.json +43 -0
- package/data/fortnite/characters/sticks.json +35 -0
- package/data/fortnite/characters/sunkenshores.json +42 -0
- package/data/fortnite/characters/sunstrider.json +42 -0
- package/data/fortnite/characters/suntanspecialist.json +43 -0
- package/data/fortnite/characters/supernovaacademy.json +27 -0
- package/data/fortnite/characters/t60powerarmor.json +39 -0
- package/data/fortnite/characters/thebattlewoods.json +44 -0
- package/data/fortnite/characters/thegreatturtle.json +37 -0
- package/data/fortnite/characters/theoperator.json +42 -0
- package/data/fortnite/characters/thequackaway.json +39 -0
- package/data/fortnite/characters/thestabilizer.json +38 -0
- package/data/fortnite/characters/theunderwriter.json +36 -0
- package/data/fortnite/characters/toyboxdocks.json +40 -0
- package/data/fortnite/characters/trace.json +43 -0
- package/data/fortnite/characters/triagetrooper.json +43 -0
- package/data/fortnite/characters/triggerfish.json +44 -0
- package/data/fortnite/characters/trogscave.json +38 -0
- package/data/fortnite/characters/utopiacity.json +27 -0
- package/data/fortnite/characters/vengeancejones.json +42 -0
- package/data/fortnite/characters/victoriasaint.json +43 -0
- package/data/fortnite/characters/vineyshafts.json +35 -0
- package/data/fortnite/characters/volpez.json +41 -0
- package/data/fortnite/characters/warmachine.json +40 -0
- package/data/fortnite/characters/wastelandermagneto.json +40 -0
- package/data/fortnite/characters/wonkeeland.json +41 -0
- package/data/fortnite/meta.json +14 -0
- package/data/game-codes/.cache-manifest.json +148 -0
- package/data/game-codes/directory.json +1196 -0
- package/data/game-codes/games/arknights-endfield.json +958 -0
- package/data/game-codes/games/borderlands-1.json +414 -0
- package/data/game-codes/games/borderlands-2.json +340 -0
- package/data/game-codes/games/borderlands-3.json +1025 -0
- package/data/game-codes/games/borderlands-4.json +3128 -0
- package/data/game-codes/games/borderlands-the-pre-sequel.json +599 -0
- package/data/game-codes/games/brawl-stars.json +4766 -0
- package/data/game-codes/games/clash-of-clans.json +4766 -0
- package/data/game-codes/games/clash-royale.json +5 -0
- package/data/game-codes/games/cookie-run-kingdom.json +5366 -0
- package/data/game-codes/games/fortnite.json +897 -0
- package/data/game-codes/games/genshin-impact.json +8520 -0
- package/data/game-codes/games/honkai-impact-3rd.json +405 -0
- package/data/game-codes/games/honkai-star-rail.json +9863 -0
- package/data/game-codes/games/neverness-to-everness.json +1695 -0
- package/data/game-codes/games/roblox-doors.json +1842 -0
- package/data/game-codes/games/roblox-rivals.json +2919 -0
- package/data/game-codes/games/tower-of-fantasy.json +2839 -0
- package/data/game-codes/games/warframe.json +20824 -0
- package/data/game-codes/games/wuthering-waves.json +2477 -0
- package/data/game-codes/games/zenless-zone-zero.json +3681 -0
- package/data/game-codes/status.json +580 -0
- package/data/genshinimpact/characters/aino.json +34 -0
- package/data/genshinimpact/characters/albedo.json +10 -9
- package/data/genshinimpact/characters/alhaitham.json +11 -10
- package/data/genshinimpact/characters/aloy.json +10 -9
- package/data/genshinimpact/characters/alyosha.json +34 -0
- package/data/genshinimpact/characters/amber.json +11 -10
- package/data/genshinimpact/characters/aratakiitto.json +11 -10
- package/data/genshinimpact/characters/arlecchino.json +11 -10
- package/data/genshinimpact/characters/baizhu.json +11 -10
- package/data/genshinimpact/characters/barbara.json +10 -9
- package/data/genshinimpact/characters/beidou.json +11 -10
- package/data/genshinimpact/characters/bennett.json +10 -9
- package/data/genshinimpact/characters/candace.json +11 -10
- package/data/genshinimpact/characters/charlotte.json +10 -9
- package/data/genshinimpact/characters/chasca.json +10 -9
- package/data/genshinimpact/characters/chevreuse.json +10 -9
- package/data/genshinimpact/characters/chiori.json +10 -9
- package/data/genshinimpact/characters/chongyun.json +11 -10
- package/data/genshinimpact/characters/citlali.json +10 -9
- package/data/genshinimpact/characters/clorinde.json +10 -9
- package/data/genshinimpact/characters/collei.json +11 -10
- package/data/genshinimpact/characters/columbina.json +34 -0
- package/data/genshinimpact/characters/cyno.json +11 -10
- package/data/genshinimpact/characters/dahlia.json +9 -8
- package/data/genshinimpact/characters/dehya.json +10 -9
- package/data/genshinimpact/characters/diluc.json +10 -9
- package/data/genshinimpact/characters/diona.json +10 -9
- package/data/genshinimpact/characters/dori.json +10 -9
- package/data/genshinimpact/characters/durin.json +10 -9
- package/data/genshinimpact/characters/emilie.json +10 -9
- package/data/genshinimpact/characters/escoffier.json +11 -10
- package/data/genshinimpact/characters/eula.json +10 -9
- package/data/genshinimpact/characters/faruzan.json +11 -10
- package/data/genshinimpact/characters/fischl.json +11 -10
- package/data/genshinimpact/characters/flins.json +34 -0
- package/data/genshinimpact/characters/freminet.json +10 -9
- package/data/genshinimpact/characters/furina.json +11 -10
- package/data/genshinimpact/characters/gaming.json +10 -9
- package/data/genshinimpact/characters/ganyu.json +10 -9
- package/data/genshinimpact/characters/gorou.json +11 -10
- package/data/genshinimpact/characters/hutao.json +11 -10
- package/data/genshinimpact/characters/iansan.json +10 -9
- package/data/genshinimpact/characters/ifa.json +34 -0
- package/data/genshinimpact/characters/illuga.json +34 -0
- package/data/genshinimpact/characters/ineffa.json +10 -9
- package/data/genshinimpact/characters/jahoda.json +34 -0
- package/data/genshinimpact/characters/jean.json +10 -9
- package/data/genshinimpact/characters/kachina.json +10 -9
- package/data/genshinimpact/characters/kaedeharakazuha.json +11 -10
- package/data/genshinimpact/characters/kaeya.json +10 -9
- package/data/genshinimpact/characters/kamisatoayaka.json +10 -9
- package/data/genshinimpact/characters/kamisatoayato.json +10 -9
- package/data/genshinimpact/characters/kaveh.json +11 -10
- package/data/genshinimpact/characters/keqing.json +10 -9
- package/data/genshinimpact/characters/kinich.json +10 -9
- package/data/genshinimpact/characters/kirara.json +10 -9
- package/data/genshinimpact/characters/klee.json +10 -9
- package/data/genshinimpact/characters/kujousara.json +10 -9
- package/data/genshinimpact/characters/kukishinobu.json +11 -10
- package/data/genshinimpact/characters/lanyan.json +10 -9
- package/data/genshinimpact/characters/lauma.json +34 -0
- package/data/genshinimpact/characters/layla.json +10 -9
- package/data/genshinimpact/characters/linnea.json +34 -0
- package/data/genshinimpact/characters/lisa.json +11 -10
- package/data/genshinimpact/characters/lohen.json +10 -9
- package/data/genshinimpact/characters/lynette.json +10 -9
- package/data/genshinimpact/characters/lyney.json +10 -9
- package/data/genshinimpact/characters/mavuika.json +10 -9
- package/data/genshinimpact/characters/mika.json +34 -0
- package/data/genshinimpact/characters/mikaschmidt.json +10 -9
- package/data/genshinimpact/characters/mona.json +11 -10
- package/data/genshinimpact/characters/mualani.json +10 -9
- package/data/genshinimpact/characters/nahida.json +11 -10
- package/data/genshinimpact/characters/navia.json +10 -9
- package/data/genshinimpact/characters/nefer.json +34 -0
- package/data/genshinimpact/characters/neuvillette.json +10 -9
- package/data/genshinimpact/characters/nicole.json +34 -0
- package/data/genshinimpact/characters/nilou.json +10 -9
- package/data/genshinimpact/characters/ningguang.json +10 -9
- package/data/genshinimpact/characters/noelle.json +11 -10
- package/data/genshinimpact/characters/odette.json +34 -0
- package/data/genshinimpact/characters/ororon.json +10 -9
- package/data/genshinimpact/characters/prune.json +9 -8
- package/data/genshinimpact/characters/qiqi.json +11 -10
- package/data/genshinimpact/characters/raidenshogun.json +11 -10
- package/data/genshinimpact/characters/razor.json +10 -9
- package/data/genshinimpact/characters/rosaria.json +11 -10
- package/data/genshinimpact/characters/sandrone.json +34 -0
- package/data/genshinimpact/characters/sangonomiyakokomi.json +11 -10
- package/data/genshinimpact/characters/sayu.json +10 -9
- package/data/genshinimpact/characters/sethos.json +10 -9
- package/data/genshinimpact/characters/shenhe.json +11 -10
- package/data/genshinimpact/characters/shikanoinheizou.json +11 -10
- package/data/genshinimpact/characters/sigewinne.json +10 -9
- package/data/genshinimpact/characters/skirk.json +10 -9
- package/data/genshinimpact/characters/sucrose.json +11 -10
- package/data/genshinimpact/characters/tartaglia.json +34 -0
- package/data/genshinimpact/characters/thoma.json +10 -9
- package/data/genshinimpact/characters/tighnari.json +11 -10
- package/data/genshinimpact/characters/traveler.json +34 -0
- package/data/genshinimpact/characters/varesa.json +10 -9
- package/data/genshinimpact/characters/varka.json +9 -8
- package/data/genshinimpact/characters/venti.json +11 -10
- package/data/genshinimpact/characters/wanderer.json +10 -9
- package/data/genshinimpact/characters/wonderlandmanekin.json +34 -0
- package/data/genshinimpact/characters/wriothesley.json +11 -10
- package/data/genshinimpact/characters/xiangling.json +11 -10
- package/data/genshinimpact/characters/xianyun.json +10 -9
- package/data/genshinimpact/characters/xiao.json +10 -9
- package/data/genshinimpact/characters/xilonen.json +10 -9
- package/data/genshinimpact/characters/xingqiu.json +11 -10
- package/data/genshinimpact/characters/xinyan.json +11 -10
- package/data/genshinimpact/characters/yaemiko.json +11 -10
- package/data/genshinimpact/characters/yanfei.json +11 -10
- package/data/genshinimpact/characters/yaoyao.json +11 -10
- package/data/genshinimpact/characters/yelan.json +10 -9
- package/data/genshinimpact/characters/yoimiya.json +11 -10
- package/data/genshinimpact/characters/yumemizukimizuki.json +10 -9
- package/data/genshinimpact/characters/yunjin.json +10 -9
- package/data/genshinimpact/characters/zhongli.json +10 -9
- package/data/genshinimpact/characters/zibai.json +10 -9
- package/data/genshinimpact/meta.json +12 -12
- package/data/honkaistarrail/characters/acheron.json +11 -10
- package/data/honkaistarrail/characters/aglaea.json +11 -10
- package/data/honkaistarrail/characters/anaxa.json +11 -10
- package/data/honkaistarrail/characters/archer.json +29 -0
- package/data/honkaistarrail/characters/argenti.json +11 -10
- package/data/honkaistarrail/characters/arlan.json +11 -10
- package/data/honkaistarrail/characters/ashveil.json +11 -10
- package/data/honkaistarrail/characters/asta.json +11 -10
- package/data/honkaistarrail/characters/aventurine.json +11 -10
- package/data/honkaistarrail/characters/bailu.json +11 -10
- package/data/honkaistarrail/characters/blackswan.json +11 -10
- package/data/honkaistarrail/characters/blade.json +11 -10
- package/data/honkaistarrail/characters/boothill.json +11 -10
- package/data/honkaistarrail/characters/bronya.json +11 -10
- package/data/honkaistarrail/characters/castorice.json +11 -10
- package/data/honkaistarrail/characters/cerydra.json +11 -10
- package/data/honkaistarrail/characters/cipher.json +11 -10
- package/data/honkaistarrail/characters/clara.json +11 -10
- package/data/honkaistarrail/characters/constance.json +11 -10
- package/data/honkaistarrail/characters/cyrene.json +11 -10
- package/data/honkaistarrail/characters/danheng.json +11 -10
- package/data/honkaistarrail/characters/danhengimbibitorlunae.json +29 -0
- package/data/honkaistarrail/characters/danhengpermansorterrae.json +29 -0
- package/data/honkaistarrail/characters/drratio.json +11 -10
- package/data/honkaistarrail/characters/evanescia.json +29 -0
- package/data/honkaistarrail/characters/evernight.json +29 -0
- package/data/honkaistarrail/characters/feixiao.json +11 -10
- package/data/honkaistarrail/characters/firefly.json +11 -10
- package/data/honkaistarrail/characters/fugue.json +29 -0
- package/data/honkaistarrail/characters/fuxuan.json +11 -10
- package/data/honkaistarrail/characters/gallagher.json +11 -10
- package/data/honkaistarrail/characters/gepard.json +11 -10
- package/data/honkaistarrail/characters/gilgamesh.json +29 -0
- package/data/honkaistarrail/characters/guinaifen.json +11 -10
- package/data/honkaistarrail/characters/hanya.json +11 -10
- package/data/honkaistarrail/characters/herta.json +11 -10
- package/data/honkaistarrail/characters/himeko.json +12 -11
- package/data/honkaistarrail/characters/himekonova.json +29 -0
- package/data/honkaistarrail/characters/hook.json +11 -10
- package/data/honkaistarrail/characters/huohuo.json +11 -10
- package/data/honkaistarrail/characters/hyacine.json +11 -10
- package/data/honkaistarrail/characters/hysilens.json +11 -10
- package/data/honkaistarrail/characters/jade.json +11 -10
- package/data/honkaistarrail/characters/jiaoqiu.json +11 -10
- package/data/honkaistarrail/characters/jingliu.json +11 -10
- package/data/honkaistarrail/characters/jingyuan.json +11 -10
- package/data/honkaistarrail/characters/kafka.json +11 -10
- package/data/honkaistarrail/characters/lingsha.json +11 -10
- package/data/honkaistarrail/characters/lisushang.json +11 -10
- package/data/honkaistarrail/characters/luka.json +11 -10
- package/data/honkaistarrail/characters/luocha.json +11 -10
- package/data/honkaistarrail/characters/lynx.json +11 -10
- package/data/honkaistarrail/characters/march7th.json +11 -10
- package/data/honkaistarrail/characters/misha.json +11 -10
- package/data/honkaistarrail/characters/mortenaxblade.json +29 -0
- package/data/honkaistarrail/characters/moze.json +11 -10
- package/data/honkaistarrail/characters/mydei.json +11 -10
- package/data/honkaistarrail/characters/natasha.json +11 -10
- package/data/honkaistarrail/characters/pela.json +11 -10
- package/data/honkaistarrail/characters/phainon.json +11 -10
- package/data/honkaistarrail/characters/qingque.json +11 -10
- package/data/honkaistarrail/characters/rappa.json +11 -10
- package/data/honkaistarrail/characters/rintohsaka.json +29 -0
- package/data/honkaistarrail/characters/robin.json +11 -10
- package/data/honkaistarrail/characters/ruanmei.json +11 -10
- package/data/honkaistarrail/characters/saber.json +29 -0
- package/data/honkaistarrail/characters/sampo.json +11 -10
- package/data/honkaistarrail/characters/seele.json +11 -10
- package/data/honkaistarrail/characters/serval.json +11 -10
- package/data/honkaistarrail/characters/silverwolf.json +11 -10
- package/data/honkaistarrail/characters/silverwolflv999.json +29 -0
- package/data/honkaistarrail/characters/sparkle.json +11 -10
- package/data/honkaistarrail/characters/sparxie.json +29 -0
- package/data/honkaistarrail/characters/sunday.json +29 -0
- package/data/honkaistarrail/characters/thedahlia.json +29 -0
- package/data/honkaistarrail/characters/theherta.json +29 -0
- package/data/honkaistarrail/characters/tingyun.json +11 -10
- package/data/honkaistarrail/characters/topaz.json +11 -10
- package/data/honkaistarrail/characters/topaznumby.json +29 -0
- package/data/honkaistarrail/characters/trailblazer.json +29 -0
- package/data/honkaistarrail/characters/tribbie.json +11 -10
- package/data/honkaistarrail/characters/welt.json +29 -0
- package/data/honkaistarrail/characters/weltyang.json +11 -10
- package/data/honkaistarrail/characters/xueyi.json +11 -10
- package/data/honkaistarrail/characters/yanqing.json +11 -10
- package/data/honkaistarrail/characters/yaoguang.json +11 -10
- package/data/honkaistarrail/characters/yukong.json +11 -10
- package/data/honkaistarrail/characters/yunli.json +11 -10
- package/data/honkaistarrail/meta.json +12 -12
- package/data/nte/characters/adler.json +13 -12
- package/data/nte/characters/aurelia.json +13 -12
- package/data/nte/characters/baicang.json +13 -12
- package/data/nte/characters/chaos.json +13 -12
- package/data/nte/characters/chiz.json +13 -12
- package/data/nte/characters/daffodill.json +13 -12
- package/data/nte/characters/edgar.json +13 -12
- package/data/nte/characters/esperzero.json +13 -12
- package/data/nte/characters/fadia.json +13 -12
- package/data/nte/characters/haniel.json +13 -12
- package/data/nte/characters/hathor.json +13 -12
- package/data/nte/characters/hotori.json +13 -12
- package/data/nte/characters/iroi.json +13 -12
- package/data/nte/characters/jiuyuan.json +13 -12
- package/data/nte/characters/lacrimosa.json +13 -12
- package/data/nte/characters/mint.json +13 -12
- package/data/nte/characters/nanally.json +13 -12
- package/data/nte/characters/sakiri.json +13 -12
- package/data/nte/characters/shinku.json +13 -12
- package/data/nte/characters/skia.json +13 -12
- package/data/nte/characters/zankou.json +51 -0
- package/data/nte/meta.json +12 -12
- package/data/toweroffantasy/characters/alyss.json +27 -0
- package/data/toweroffantasy/characters/annabella.json +23 -0
- package/data/toweroffantasy/characters/asuka.json +23 -0
- package/data/toweroffantasy/characters/bailing.json +23 -0
- package/data/toweroffantasy/characters/brevey.json +23 -0
- package/data/toweroffantasy/characters/claudia.json +23 -0
- package/data/toweroffantasy/characters/cobaltb.json +23 -0
- package/data/toweroffantasy/characters/cocoritter.json +23 -0
- package/data/toweroffantasy/characters/crow.json +23 -0
- package/data/toweroffantasy/characters/echo.json +23 -0
- package/data/toweroffantasy/characters/ene.json +23 -0
- package/data/toweroffantasy/characters/feise.json +25 -0
- package/data/toweroffantasy/characters/fenrir.json +23 -0
- package/data/toweroffantasy/characters/fiona.json +25 -0
- package/data/toweroffantasy/characters/frigg.json +23 -0
- package/data/toweroffantasy/characters/gnonno.json +23 -0
- package/data/toweroffantasy/characters/hilda.json +23 -0
- package/data/toweroffantasy/characters/huang.json +25 -0
- package/data/toweroffantasy/characters/huma.json +25 -0
- package/data/toweroffantasy/characters/icarus.json +23 -0
- package/data/toweroffantasy/characters/jiyu.json +25 -0
- package/data/toweroffantasy/characters/king.json +23 -0
- package/data/toweroffantasy/characters/lan.json +25 -0
- package/data/toweroffantasy/characters/lin.json +23 -0
- package/data/toweroffantasy/characters/linghan.json +23 -0
- package/data/toweroffantasy/characters/liuhuo.json +23 -0
- package/data/toweroffantasy/characters/lyra.json +23 -0
- package/data/toweroffantasy/characters/meryl.json +23 -0
- package/data/toweroffantasy/characters/mingjing.json +27 -0
- package/data/toweroffantasy/characters/nanyin.json +25 -0
- package/data/toweroffantasy/characters/nemesis.json +25 -0
- package/data/toweroffantasy/characters/pepper.json +23 -0
- package/data/toweroffantasy/characters/plotti.json +25 -0
- package/data/toweroffantasy/characters/roslyn.json +23 -0
- package/data/toweroffantasy/characters/rubilia.json +25 -0
- package/data/toweroffantasy/characters/ruby.json +25 -0
- package/data/toweroffantasy/characters/sakifuwa.json +23 -0
- package/data/toweroffantasy/characters/samir.json +23 -0
- package/data/toweroffantasy/characters/shiro.json +25 -0
- package/data/toweroffantasy/characters/tianlang.json +25 -0
- package/data/toweroffantasy/characters/tsubasa.json +23 -0
- package/data/toweroffantasy/characters/umi.json +23 -0
- package/data/toweroffantasy/characters/yanmiao.json +25 -0
- package/data/toweroffantasy/characters/yanuo.json +23 -0
- package/data/toweroffantasy/characters/yulan.json +23 -0
- package/data/toweroffantasy/characters/zero.json +23 -0
- package/data/toweroffantasy/meta.json +14 -0
- package/data/vrchat/groups/clubzodiac.json +2 -1
- package/data/vrchat/groups/dirtydancers.json +2 -1
- package/data/vrchat/groups/drumsynews.json +2 -1
- package/data/vrchat/groups/ebola20.json +2 -1
- package/data/vrchat/groups/internationaldancersassociation.json +2 -1
- package/data/vrchat/groups/kannagang.json +2 -1
- package/data/vrchat/groups/lolisquad.json +2 -1
- package/data/vrchat/groups/nekonightsnightclub.json +2 -1
- package/data/vrchat/groups/nekostudios.json +2 -1
- package/data/vrchat/groups/nekosunecommunity.json +2 -1
- package/data/vrchat/groups/voidclub.json +2 -1
- package/data/vrchat/groups/voltdanceclub.json +2 -1
- package/data/vrchat/groups/vrceventsgroup.json +2 -1
- package/data/vrchat/groups/vrchatcommunitydiscord.json +2 -1
- package/data/vrchat/meta.json +11 -11
- package/data/vrchat/players/alto.json +8 -7
- package/data/vrchat/players/arcadum.json +9 -8
- package/data/vrchat/players/chairhandler.json +8 -7
- package/data/vrchat/players/chipz.json +8 -7
- package/data/vrchat/players/hikarivr.json +10 -9
- package/data/vrchat/players/kimple.json +9 -8
- package/data/vrchat/players/linkzr.json +7 -6
- package/data/vrchat/players/nagzz21.json +8 -7
- package/data/vrchat/players/nebride.json +9 -8
- package/data/vrchat/players/nekosunevr.json +10 -9
- package/data/vrchat/players/pokelawls.json +8 -7
- package/data/vrchat/players/qwonk.json +7 -6
- package/data/vrchat/players/roflgator.json +9 -8
- package/data/vrchat/players/scifri.json +6 -5
- package/data/vrchat/players/sodapoppin.json +8 -7
- package/data/vrchat/players/woops.json +8 -7
- package/data/vrchat/players/zentreya.json +9 -8
- package/data/vrchat/worlds/ascensionacademy.json +6 -5
- package/data/vrchat/worlds/clubvgl.json +7 -6
- package/data/vrchat/worlds/fightfinder.json +7 -6
- package/data/vrchat/worlds/hangouthouse.json +6 -5
- package/data/vrchat/worlds/nekostudioshalloween.json +8 -7
- package/data/vrchat/worlds/nekostudiosnewyear.json +8 -7
- package/data/vrchat/worlds/nekostudiosxmas.json +8 -7
- package/data/vrchat/worlds/nekosunevranimations2023.json +8 -7
- package/data/vrchat/worlds/nekosunevranimations20232024.json +10 -0
- package/data/vrchat/worlds/nekosunevranimations2024.json +8 -7
- package/data/vrchat/worlds/nekosunevranimations2025.json +8 -7
- package/data/vrchat/worlds/rumhamsavatarworld.json +6 -5
- package/data/vrchat/worlds/smewsranch.json +6 -5
- package/data/vrchat/worlds/smewsroom.json +6 -5
- package/data/vrchat/worlds/sunsetbar.json +6 -5
- package/data/vrchat/worlds/theblackcat.json +9 -8
- package/data/vrchat/worlds/thegreatpug.json +7 -6
- package/data/vrchat/worlds/themidnightbar.json +6 -5
- package/data/vrchat/worlds/voidclub.json +6 -5
- package/data/vrchat/worlds/voltdanceclub.json +6 -5
- package/data/vrchat/worlds/vrchathome.json +7 -6
- package/data/vrchat/worlds/whatalegend.json +5 -4
- package/data/warframe/characters/amesha.json +226 -0
- package/data/warframe/characters/ash.json +1354 -0
- package/data/warframe/characters/ashprime.json +1994 -0
- package/data/warframe/characters/atlas.json +916 -0
- package/data/warframe/characters/atlasprime.json +1632 -0
- package/data/warframe/characters/banshee.json +1036 -0
- package/data/warframe/characters/bansheeprime.json +1750 -0
- package/data/warframe/characters/baruuk.json +555 -0
- package/data/warframe/characters/baruukprime.json +1298 -0
- package/data/warframe/characters/bonewidow.json +354 -0
- package/data/warframe/characters/caliban.json +434 -0
- package/data/warframe/characters/calibanprime.json +683 -0
- package/data/warframe/characters/chroma.json +1344 -0
- package/data/warframe/characters/chromaprime.json +1977 -0
- package/data/warframe/characters/citrine.json +220 -0
- package/data/warframe/characters/cyte09.json +218 -0
- package/data/warframe/characters/dagath.json +194 -0
- package/data/warframe/characters/dante.json +223 -0
- package/data/warframe/characters/elytron.json +300 -0
- package/data/warframe/characters/ember.json +1209 -0
- package/data/warframe/characters/emberprime.json +1678 -0
- package/data/warframe/characters/equinox.json +958 -0
- package/data/warframe/characters/equinoxprime.json +1644 -0
- package/data/warframe/characters/excalibur.json +1603 -0
- package/data/warframe/characters/excaliburprime.json +1524 -0
- package/data/warframe/characters/excaliburumbra.json +397 -0
- package/data/warframe/characters/follie.json +246 -0
- package/data/warframe/characters/frost.json +1330 -0
- package/data/warframe/characters/frostprime.json +1686 -0
- package/data/warframe/characters/gara.json +925 -0
- package/data/warframe/characters/garaprime.json +1494 -0
- package/data/warframe/characters/garuda.json +851 -0
- package/data/warframe/characters/garudaprime.json +1449 -0
- package/data/warframe/characters/gauss.json +506 -0
- package/data/warframe/characters/gaussprime.json +1077 -0
- package/data/warframe/characters/grendel.json +463 -0
- package/data/warframe/characters/grendelprime.json +772 -0
- package/data/warframe/characters/gyre.json +381 -0
- package/data/warframe/characters/gyreprime.json +403 -0
- package/data/warframe/characters/harrow.json +916 -0
- package/data/warframe/characters/harrowprime.json +1700 -0
- package/data/warframe/characters/helminth.json +220 -0
- package/data/warframe/characters/hildryn.json +627 -0
- package/data/warframe/characters/hildrynprime.json +996 -0
- package/data/warframe/characters/hydroid.json +1028 -0
- package/data/warframe/characters/hydroidprime.json +1621 -0
- package/data/warframe/characters/inaros.json +976 -0
- package/data/warframe/characters/inarosprime.json +1721 -0
- package/data/warframe/characters/itzal.json +465 -0
- package/data/warframe/characters/ivara.json +1349 -0
- package/data/warframe/characters/ivaraprime.json +1921 -0
- package/data/warframe/characters/jade.json +240 -0
- package/data/warframe/characters/khora.json +804 -0
- package/data/warframe/characters/khoraprime.json +1403 -0
- package/data/warframe/characters/koumei.json +221 -0
- package/data/warframe/characters/kullervo.json +191 -0
- package/data/warframe/characters/lavos.json +511 -0
- package/data/warframe/characters/lavosprime.json +711 -0
- package/data/warframe/characters/limbo.json +1363 -0
- package/data/warframe/characters/limboprime.json +2084 -0
- package/data/warframe/characters/loki.json +1209 -0
- package/data/warframe/characters/lokiprime.json +1457 -0
- package/data/warframe/characters/mag.json +2089 -0
- package/data/warframe/characters/magprime.json +2532 -0
- package/data/warframe/characters/mesa.json +1195 -0
- package/data/warframe/characters/mesaprime.json +2059 -0
- package/data/warframe/characters/mirage.json +1561 -0
- package/data/warframe/characters/mirageprime.json +2257 -0
- package/data/warframe/characters/nekros.json +1235 -0
- package/data/warframe/characters/nekrosprime.json +1882 -0
- package/data/warframe/characters/nezha.json +868 -0
- package/data/warframe/characters/nezhaprime.json +1695 -0
- package/data/warframe/characters/nidus.json +1027 -0
- package/data/warframe/characters/nidusprime.json +1706 -0
- package/data/warframe/characters/nokko.json +308 -0
- package/data/warframe/characters/nova.json +1226 -0
- package/data/warframe/characters/novaprime.json +1667 -0
- package/data/warframe/characters/nyx.json +1610 -0
- package/data/warframe/characters/nyxprime.json +1967 -0
- package/data/warframe/characters/oberon.json +1249 -0
- package/data/warframe/characters/oberonprime.json +1872 -0
- package/data/warframe/characters/octavia.json +1049 -0
- package/data/warframe/characters/octaviaprime.json +1735 -0
- package/data/warframe/characters/odonata.json +336 -0
- package/data/warframe/characters/odonataprime.json +787 -0
- package/data/warframe/characters/oraxia.json +246 -0
- package/data/warframe/characters/orionsirius.json +134 -0
- package/data/warframe/characters/protea.json +521 -0
- package/data/warframe/characters/proteaprime.json +992 -0
- package/data/warframe/characters/qorvex.json +214 -0
- package/data/warframe/characters/revenant.json +931 -0
- package/data/warframe/characters/revenantprime.json +1494 -0
- package/data/warframe/characters/rhino.json +1344 -0
- package/data/warframe/characters/rhinoprime.json +1675 -0
- package/data/warframe/characters/saryn.json +1210 -0
- package/data/warframe/characters/sarynprime.json +2042 -0
- package/data/warframe/characters/sevagoth.json +478 -0
- package/data/warframe/characters/sevagothprime.json +910 -0
- package/data/warframe/characters/siriusorion.json +247 -0
- package/data/warframe/characters/styanax.json +320 -0
- package/data/warframe/characters/styanaxprime.json +321 -0
- package/data/warframe/characters/temple.json +271 -0
- package/data/warframe/characters/titania.json +1227 -0
- package/data/warframe/characters/titaniaprime.json +2000 -0
- package/data/warframe/characters/trinity.json +1097 -0
- package/data/warframe/characters/trinityprime.json +1849 -0
- package/data/warframe/characters/uriel.json +215 -0
- package/data/warframe/characters/valkyr.json +1165 -0
- package/data/warframe/characters/valkyrprime.json +1969 -0
- package/data/warframe/characters/vauban.json +1067 -0
- package/data/warframe/characters/vaubanprime.json +1614 -0
- package/data/warframe/characters/voidrig.json +355 -0
- package/data/warframe/characters/volt.json +1324 -0
- package/data/warframe/characters/voltprime.json +1843 -0
- package/data/warframe/characters/voruna.json +293 -0
- package/data/warframe/characters/vorunaprime.json +431 -0
- package/data/warframe/characters/wisp.json +824 -0
- package/data/warframe/characters/wispprime.json +908 -0
- package/data/warframe/characters/wukong.json +927 -0
- package/data/warframe/characters/wukongprime.json +1754 -0
- package/data/warframe/characters/xaku.json +620 -0
- package/data/warframe/characters/xakuprime.json +879 -0
- package/data/warframe/characters/yareli.json +409 -0
- package/data/warframe/characters/yareliprime.json +683 -0
- package/data/warframe/characters/zephyr.json +1149 -0
- package/data/warframe/characters/zephyrprime.json +1864 -0
- package/data/warframe/meta.json +14 -0
- package/data/wutheringwaves/meta.json +14 -0
- package/data/zenlesszonezero/meta.json +14 -0
- package/gamecodes.js +204 -0
- package/index.js +36 -17
- package/package.json +33 -3
- package/scripts/cache-sync-smoke.js +139 -0
- package/scripts/game-code-cache-smoke.js +125 -0
- package/scripts/sync-all-cache.js +18 -0
- package/scripts/sync-api-cache.js +368 -0
- package/scripts/sync-game-code-cache.js +283 -0
- package/scripts/sync-gaming-info-cache.js +46 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { syncGamingInfoCacheSafely } = require('./sync-gaming-info-cache');
|
|
4
|
+
const { syncGameCodeCache } = require('./sync-game-code-cache');
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
// The gaming-info phase temporarily moves data/game-codes out of the way,
|
|
8
|
+
// mirrors data/, then restores the previous code cache before code syncing.
|
|
9
|
+
// If the code API fails afterwards, its last-good fallback is still present.
|
|
10
|
+
const gamingInfos = await syncGamingInfoCacheSafely();
|
|
11
|
+
const gameCodes = await syncGameCodeCache();
|
|
12
|
+
console.log(JSON.stringify({ success: true, gamingInfos, gameCodes }, null, 2));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
main().catch((error) => {
|
|
16
|
+
console.error(`[gaming-infos combined cache] ${error.stack || error.message || error}`);
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
});
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const ROOT_DIR = path.resolve(__dirname, '..');
|
|
8
|
+
const DEFAULT_DATA_DIR = path.join(ROOT_DIR, 'data');
|
|
9
|
+
const DEFAULT_API_BASE = 'https://api.nekosunevr.co.uk';
|
|
10
|
+
const DEFAULT_API_PATH = '/v5/games/api/gaming-infos';
|
|
11
|
+
|
|
12
|
+
// Safety baseline for older APINODE deployments. Newer APINODE root responses
|
|
13
|
+
// expose { slug, types } for every game, so the mirror automatically adds any
|
|
14
|
+
// newly published datasets (including the full Call of Duty catalog) without a
|
|
15
|
+
// package-code change. Explicit GAMING_INFOS_CACHE_MANIFEST_JSON still works.
|
|
16
|
+
const DEFAULT_MANIFEST = Object.freeze({
|
|
17
|
+
vrchat: ['players', 'groups', 'worlds', 'avatars'],
|
|
18
|
+
genshinimpact: ['characters'],
|
|
19
|
+
honkaistarrail: ['characters'],
|
|
20
|
+
nte: ['characters'],
|
|
21
|
+
wutheringwaves: ['characters'],
|
|
22
|
+
warframe: ['characters'],
|
|
23
|
+
fortnite: ['characters', 'npcs', 'maps'],
|
|
24
|
+
zenlesszonezero: ['characters'],
|
|
25
|
+
toweroffantasy: ['characters'],
|
|
26
|
+
arknightsendfield: ['characters']
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function numberOption(value, fallback, min = 0, max = Number.MAX_SAFE_INTEGER) {
|
|
30
|
+
const parsed = Number(value);
|
|
31
|
+
return Number.isFinite(parsed) ? Math.min(max, Math.max(min, parsed)) : fallback;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sleep(ms) {
|
|
35
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeBase(value) {
|
|
39
|
+
return String(value || DEFAULT_API_BASE).replace(/\/+$/, '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeApiPath(value) {
|
|
43
|
+
const raw = String(value || DEFAULT_API_PATH).trim();
|
|
44
|
+
return `/${raw.replace(/^\/+|\/+$/g, '')}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function safeSlug(value, label = 'slug') {
|
|
48
|
+
const slug = String(value || '').trim().toLowerCase();
|
|
49
|
+
if (!slug || !/^[a-z0-9][a-z0-9._-]*$/.test(slug)) {
|
|
50
|
+
throw new Error(`Invalid ${label}: ${JSON.stringify(value)}`);
|
|
51
|
+
}
|
|
52
|
+
return slug;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function fallbackSlug(name) {
|
|
56
|
+
const slug = String(name || '')
|
|
57
|
+
.normalize('NFKD')
|
|
58
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
59
|
+
.toLowerCase()
|
|
60
|
+
.replace(/[^a-z0-9]+/g, '')
|
|
61
|
+
.trim();
|
|
62
|
+
return safeSlug(slug, 'derived slug');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeManifestObject(parsed) {
|
|
66
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
|
|
67
|
+
throw new Error('Gaming Infos cache manifest must be a JSON object');
|
|
68
|
+
}
|
|
69
|
+
const result = {};
|
|
70
|
+
for (const [gameRaw, typesRaw] of Object.entries(parsed)) {
|
|
71
|
+
const game = safeSlug(gameRaw, 'game');
|
|
72
|
+
if (!Array.isArray(typesRaw) || !typesRaw.length) throw new Error(`No types configured for ${game}`);
|
|
73
|
+
result[game] = [...new Set(typesRaw.map((type) => safeSlug(type, 'type')))];
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function loadManifest(raw = process.env.GAMING_INFOS_CACHE_MANIFEST_JSON) {
|
|
79
|
+
if (!raw) return { ...DEFAULT_MANIFEST };
|
|
80
|
+
return normalizeManifestObject(typeof raw === 'string' ? JSON.parse(raw) : raw);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function mergeDiscoveredManifest(rootData, configuredManifest = DEFAULT_MANIFEST) {
|
|
84
|
+
const result = normalizeManifestObject(configuredManifest);
|
|
85
|
+
for (const row of Array.isArray(rootData) ? rootData : []) {
|
|
86
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) continue;
|
|
87
|
+
if (!row.slug || !Array.isArray(row.types) || !row.types.length) continue;
|
|
88
|
+
const game = safeSlug(row.slug, 'root game slug');
|
|
89
|
+
const types = [...new Set(row.types.map((type) => safeSlug(type, `type for ${game}`)))];
|
|
90
|
+
if (types.length) result[game] = types;
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function requestJson(url, options = {}) {
|
|
96
|
+
const timeoutMs = numberOption(options.timeoutMs ?? process.env.GAMING_INFOS_CACHE_TIMEOUT_MS, 15000, 1000, 120000);
|
|
97
|
+
const retries = numberOption(options.retries ?? process.env.GAMING_INFOS_CACHE_RETRIES, 2, 0, 5);
|
|
98
|
+
const retryDelayMs = numberOption(options.retryDelayMs ?? process.env.GAMING_INFOS_CACHE_RETRY_DELAY_MS, 750, 0, 30000);
|
|
99
|
+
let lastError;
|
|
100
|
+
|
|
101
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
104
|
+
try {
|
|
105
|
+
const response = await fetch(url, {
|
|
106
|
+
signal: controller.signal,
|
|
107
|
+
headers: {
|
|
108
|
+
Accept: 'application/json',
|
|
109
|
+
'User-Agent': process.env.GAMING_INFOS_CACHE_USER_AGENT || 'NekoSuneProjects-gaming-infos-cache/1.0'
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
const error = new Error(`HTTP ${response.status} from ${url}`);
|
|
115
|
+
error.status = response.status;
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const body = await response.json();
|
|
120
|
+
if (!body || body.success === false) {
|
|
121
|
+
throw new Error(`API returned an unsuccessful response for ${url}`);
|
|
122
|
+
}
|
|
123
|
+
if (options.rawEnvelope) return body;
|
|
124
|
+
return body.data !== undefined ? body.data : body;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
lastError = error;
|
|
127
|
+
const status = Number(error.status || 0);
|
|
128
|
+
const retryable = !status || status === 408 || status === 425 || status === 429 || status >= 500;
|
|
129
|
+
if (attempt >= retries || !retryable) break;
|
|
130
|
+
await sleep(retryDelayMs * (attempt + 1));
|
|
131
|
+
} finally {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw lastError || new Error(`Failed to fetch ${url}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function apiUrl(base, apiPath, suffix = '') {
|
|
140
|
+
return `${normalizeBase(base)}${normalizeApiPath(apiPath)}${suffix}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function validateRootSyncState(rootEnvelope) {
|
|
144
|
+
const sync = rootEnvelope && typeof rootEnvelope === 'object' ? rootEnvelope.sync : null;
|
|
145
|
+
if (!sync || typeof sync !== 'object') return;
|
|
146
|
+
|
|
147
|
+
const state = sync.state && typeof sync.state === 'object' ? sync.state : null;
|
|
148
|
+
const status = String(state?.lastStatus || '').toLowerCase();
|
|
149
|
+
if (sync.running || status === 'running') {
|
|
150
|
+
throw new Error('APINODE content synchronization is currently running; preserving the previous fallback until it is idle');
|
|
151
|
+
}
|
|
152
|
+
if (state && ['never-run', 'failed'].includes(status)) {
|
|
153
|
+
throw new Error(`APINODE content synchronization state is ${status}; preserving the previous fallback`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const persistedSchema = Number(state?.syncSchemaVersion || 0);
|
|
157
|
+
const requiredSchema = Number(state?.requiredSyncSchemaVersion || sync.syncSchemaVersion || 0);
|
|
158
|
+
if (requiredSchema > 0 && persistedSchema < requiredSchema) {
|
|
159
|
+
throw new Error(`APINODE content schema catch-up is incomplete (${persistedSchema}/${requiredSchema}); preserving the previous fallback`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function writeJson(filePath, value) {
|
|
164
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
165
|
+
await fs.promises.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function walkFiles(dir, root = dir, out = new Map()) {
|
|
169
|
+
if (!fs.existsSync(dir)) return out;
|
|
170
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
const full = path.join(dir, entry.name);
|
|
173
|
+
if (entry.isDirectory()) await walkFiles(full, root, out);
|
|
174
|
+
else if (entry.isFile() && entry.name !== '.cache-manifest.json') {
|
|
175
|
+
const relative = path.relative(root, full).split(path.sep).join('/');
|
|
176
|
+
out.set(relative, await fs.promises.readFile(full));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function hashFiles(files) {
|
|
183
|
+
const hash = crypto.createHash('sha256');
|
|
184
|
+
for (const [name, content] of [...files.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
185
|
+
hash.update(name);
|
|
186
|
+
hash.update('\0');
|
|
187
|
+
hash.update(content);
|
|
188
|
+
hash.update('\0');
|
|
189
|
+
}
|
|
190
|
+
return hash.digest('hex');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function snapshotHash(dir) {
|
|
194
|
+
return hashFiles(await walkFiles(dir));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function diffStats(oldFiles, newFiles) {
|
|
198
|
+
let added = 0;
|
|
199
|
+
let removed = 0;
|
|
200
|
+
let changed = 0;
|
|
201
|
+
let unchanged = 0;
|
|
202
|
+
|
|
203
|
+
for (const [name, content] of newFiles) {
|
|
204
|
+
if (!oldFiles.has(name)) added += 1;
|
|
205
|
+
else if (!oldFiles.get(name).equals(content)) changed += 1;
|
|
206
|
+
else unchanged += 1;
|
|
207
|
+
}
|
|
208
|
+
for (const name of oldFiles.keys()) if (!newFiles.has(name)) removed += 1;
|
|
209
|
+
return { added, changed, removed, unchanged, total: newFiles.size };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function replaceSnapshot(nextDir, dataDir) {
|
|
213
|
+
const backupDir = `${dataDir}.cache-backup-${process.pid}`;
|
|
214
|
+
await fs.promises.rm(backupDir, { recursive: true, force: true });
|
|
215
|
+
const hadCurrent = fs.existsSync(dataDir);
|
|
216
|
+
|
|
217
|
+
if (hadCurrent) await fs.promises.rename(dataDir, backupDir);
|
|
218
|
+
try {
|
|
219
|
+
await fs.promises.rename(nextDir, dataDir);
|
|
220
|
+
await fs.promises.rm(backupDir, { recursive: true, force: true });
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (!fs.existsSync(dataDir) && hadCurrent && fs.existsSync(backupDir)) {
|
|
223
|
+
await fs.promises.rename(backupDir, dataDir);
|
|
224
|
+
}
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function normalizedEntry(item) {
|
|
230
|
+
if (!item || Array.isArray(item) || typeof item !== 'object') throw new Error('API list contained a non-object entry');
|
|
231
|
+
const slug = safeSlug(item.slug || fallbackSlug(item.name), 'entry slug');
|
|
232
|
+
return { ...item, slug };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function buildNextSnapshot(options) {
|
|
236
|
+
const { apiBase, apiPath, dataDir, manifest: configuredManifest } = options;
|
|
237
|
+
const parentDir = path.dirname(dataDir);
|
|
238
|
+
const nextDir = path.join(parentDir, `.gaming-infos-cache-next-${process.pid}-${Date.now()}`);
|
|
239
|
+
await fs.promises.rm(nextDir, { recursive: true, force: true });
|
|
240
|
+
await fs.promises.mkdir(nextDir, { recursive: true });
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const rootEnvelope = await requestJson(apiUrl(apiBase, apiPath), { ...options, rawEnvelope: true });
|
|
244
|
+
validateRootSyncState(rootEnvelope);
|
|
245
|
+
const rootData = rootEnvelope.data !== undefined ? rootEnvelope.data : rootEnvelope;
|
|
246
|
+
if (!Array.isArray(rootData) || !rootData.length) {
|
|
247
|
+
throw new Error('Gaming-infos root endpoint returned no games; refusing to replace the fallback cache');
|
|
248
|
+
}
|
|
249
|
+
const manifest = mergeDiscoveredManifest(rootData, configuredManifest);
|
|
250
|
+
|
|
251
|
+
const counts = {};
|
|
252
|
+
for (const [game, types] of Object.entries(manifest)) {
|
|
253
|
+
const meta = await requestJson(apiUrl(apiBase, apiPath, `/${encodeURIComponent(game)}/meta/${encodeURIComponent(game)}`), options);
|
|
254
|
+
if (!meta || Array.isArray(meta) || typeof meta !== 'object') throw new Error(`Invalid meta payload for ${game}`);
|
|
255
|
+
await writeJson(path.join(nextDir, game, 'meta.json'), meta);
|
|
256
|
+
counts[game] = { meta: 1 };
|
|
257
|
+
|
|
258
|
+
for (const type of types) {
|
|
259
|
+
const list = await requestJson(apiUrl(apiBase, apiPath, `/${encodeURIComponent(game)}/${encodeURIComponent(type)}`), options);
|
|
260
|
+
if (!Array.isArray(list)) throw new Error(`Expected an array for ${game}/${type}`);
|
|
261
|
+
|
|
262
|
+
const seen = new Set();
|
|
263
|
+
counts[game][type] = list.length;
|
|
264
|
+
for (const rawItem of list) {
|
|
265
|
+
const item = normalizedEntry(rawItem);
|
|
266
|
+
if (seen.has(item.slug)) throw new Error(`Duplicate slug ${item.slug} in ${game}/${type}`);
|
|
267
|
+
seen.add(item.slug);
|
|
268
|
+
await writeJson(path.join(nextDir, game, type, `${item.slug}.json`), item);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { nextDir, rootGameCount: rootData.length, counts, manifest };
|
|
274
|
+
} catch (error) {
|
|
275
|
+
await fs.promises.rm(nextDir, { recursive: true, force: true }).catch(() => {});
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function syncApiCache(options = {}) {
|
|
281
|
+
if (typeof fetch !== 'function') throw new Error('Node.js 18+ is required because this sync uses the built-in fetch API');
|
|
282
|
+
|
|
283
|
+
const dataDir = path.resolve(options.dataDir || process.env.GAMING_INFOS_CACHE_DIR || DEFAULT_DATA_DIR);
|
|
284
|
+
const apiBase = options.apiBase || process.env.GAMING_INFOS_API_BASE || DEFAULT_API_BASE;
|
|
285
|
+
const apiPath = options.apiPath || process.env.GAMING_INFOS_API_PATH || DEFAULT_API_PATH;
|
|
286
|
+
const manifest = options.manifest || loadManifest();
|
|
287
|
+
const requestOptions = {
|
|
288
|
+
...options,
|
|
289
|
+
apiBase,
|
|
290
|
+
apiPath,
|
|
291
|
+
dataDir,
|
|
292
|
+
manifest
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
let nextDir;
|
|
296
|
+
try {
|
|
297
|
+
const built = await buildNextSnapshot(requestOptions);
|
|
298
|
+
nextDir = built.nextDir;
|
|
299
|
+
const effectiveManifest = built.manifest;
|
|
300
|
+
const oldFiles = await walkFiles(dataDir);
|
|
301
|
+
const newFiles = await walkFiles(nextDir);
|
|
302
|
+
const oldHash = hashFiles(oldFiles);
|
|
303
|
+
const newHash = hashFiles(newFiles);
|
|
304
|
+
const stats = diffStats(oldFiles, newFiles);
|
|
305
|
+
|
|
306
|
+
if (oldFiles.size && oldHash === newHash) {
|
|
307
|
+
await fs.promises.rm(nextDir, { recursive: true, force: true });
|
|
308
|
+
return {
|
|
309
|
+
success: true,
|
|
310
|
+
changed: false,
|
|
311
|
+
contentHash: newHash,
|
|
312
|
+
source: apiUrl(apiBase, apiPath),
|
|
313
|
+
games: Object.keys(effectiveManifest),
|
|
314
|
+
counts: built.counts,
|
|
315
|
+
stats
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const cacheManifest = {
|
|
320
|
+
schemaVersion: 2,
|
|
321
|
+
source: apiUrl(apiBase, apiPath),
|
|
322
|
+
syncedAt: new Date().toISOString(),
|
|
323
|
+
contentHash: newHash,
|
|
324
|
+
rootGameCount: built.rootGameCount,
|
|
325
|
+
autoDiscoveredTypes: true,
|
|
326
|
+
games: Object.fromEntries(Object.entries(effectiveManifest).map(([game, types]) => [game, { types, counts: built.counts[game] }])),
|
|
327
|
+
stats
|
|
328
|
+
};
|
|
329
|
+
await writeJson(path.join(nextDir, '.cache-manifest.json'), cacheManifest);
|
|
330
|
+
await replaceSnapshot(nextDir, dataDir);
|
|
331
|
+
nextDir = null;
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
success: true,
|
|
335
|
+
changed: true,
|
|
336
|
+
contentHash: newHash,
|
|
337
|
+
source: cacheManifest.source,
|
|
338
|
+
games: Object.keys(effectiveManifest),
|
|
339
|
+
counts: built.counts,
|
|
340
|
+
stats
|
|
341
|
+
};
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (nextDir) await fs.promises.rm(nextDir, { recursive: true, force: true }).catch(() => {});
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function main() {
|
|
349
|
+
const result = await syncApiCache();
|
|
350
|
+
console.log(JSON.stringify(result, null, 2));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (require.main === module) {
|
|
354
|
+
main().catch((error) => {
|
|
355
|
+
console.error(`[gaming-infos cache] ${error.stack || error.message || error}`);
|
|
356
|
+
process.exitCode = 1;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
module.exports = {
|
|
361
|
+
DEFAULT_MANIFEST,
|
|
362
|
+
loadManifest,
|
|
363
|
+
mergeDiscoveredManifest,
|
|
364
|
+
validateRootSyncState,
|
|
365
|
+
normalizedEntry,
|
|
366
|
+
snapshotHash,
|
|
367
|
+
syncApiCache
|
|
368
|
+
};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const ROOT_DIR = path.resolve(__dirname, '..');
|
|
8
|
+
const DEFAULT_DATA_DIR = path.join(ROOT_DIR, 'data', 'game-codes');
|
|
9
|
+
const DEFAULT_API_BASE = 'https://api.nekosunevr.co.uk';
|
|
10
|
+
const DEFAULT_API_PATH = '/v5/games/api/codes';
|
|
11
|
+
|
|
12
|
+
function numberOption(value, fallback, min = 0, max = Number.MAX_SAFE_INTEGER) {
|
|
13
|
+
const parsed = Number(value);
|
|
14
|
+
return Number.isFinite(parsed) ? Math.min(max, Math.max(min, parsed)) : fallback;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sleep(ms) {
|
|
18
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeBase(value) {
|
|
22
|
+
return String(value || DEFAULT_API_BASE).replace(/\/+$/, '');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeApiPath(value) {
|
|
26
|
+
const raw = String(value || DEFAULT_API_PATH).trim();
|
|
27
|
+
return `/${raw.replace(/^\/+|\/+$/g, '')}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function requestJson(url, options = {}) {
|
|
31
|
+
const timeoutMs = numberOption(options.timeoutMs ?? process.env.GAME_CODES_CACHE_TIMEOUT_MS, 15000, 1000, 120000);
|
|
32
|
+
const retries = numberOption(options.retries ?? process.env.GAME_CODES_CACHE_RETRIES, 2, 0, 5);
|
|
33
|
+
const retryDelayMs = numberOption(options.retryDelayMs ?? process.env.GAME_CODES_CACHE_RETRY_DELAY_MS, 750, 0, 30000);
|
|
34
|
+
let lastError;
|
|
35
|
+
|
|
36
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(url, {
|
|
41
|
+
signal: controller.signal,
|
|
42
|
+
headers: {
|
|
43
|
+
Accept: 'application/json',
|
|
44
|
+
'User-Agent': process.env.GAME_CODES_CACHE_USER_AGENT || 'NekoSuneProjects-gaming-infos-game-codes-cache/1.0'
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const error = new Error(`HTTP ${response.status} from ${url}`);
|
|
49
|
+
error.status = response.status;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
const body = await response.json();
|
|
53
|
+
if (!body || body.success === false) throw new Error(`API returned an unsuccessful response for ${url}`);
|
|
54
|
+
return body;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
lastError = error;
|
|
57
|
+
const status = Number(error.status || 0);
|
|
58
|
+
const retryable = !status || status === 408 || status === 425 || status === 429 || status >= 500;
|
|
59
|
+
if (attempt >= retries || !retryable) break;
|
|
60
|
+
await sleep(retryDelayMs * (attempt + 1));
|
|
61
|
+
} finally {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
throw lastError || new Error(`Failed to fetch ${url}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function apiUrl(base, apiPath, suffix = '', query = {}) {
|
|
70
|
+
const url = new URL(`${normalizeBase(base)}${normalizeApiPath(apiPath)}${suffix}`);
|
|
71
|
+
for (const [key, value] of Object.entries(query)) {
|
|
72
|
+
if (value !== undefined && value !== null && value !== '') url.searchParams.set(key, String(value));
|
|
73
|
+
}
|
|
74
|
+
return url.href;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function writeJson(filePath, value) {
|
|
78
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
79
|
+
await fs.promises.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function walkFiles(dir, root = dir, out = new Map()) {
|
|
83
|
+
if (!fs.existsSync(dir)) return out;
|
|
84
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
const full = path.join(dir, entry.name);
|
|
87
|
+
if (entry.isDirectory()) await walkFiles(full, root, out);
|
|
88
|
+
else if (entry.isFile() && entry.name !== '.cache-manifest.json') {
|
|
89
|
+
const relative = path.relative(root, full).split(path.sep).join('/');
|
|
90
|
+
out.set(relative, await fs.promises.readFile(full));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function hashFiles(files) {
|
|
97
|
+
const hash = crypto.createHash('sha256');
|
|
98
|
+
for (const [name, content] of [...files.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
99
|
+
hash.update(name);
|
|
100
|
+
hash.update('\0');
|
|
101
|
+
hash.update(content);
|
|
102
|
+
hash.update('\0');
|
|
103
|
+
}
|
|
104
|
+
return hash.digest('hex');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function diffStats(oldFiles, newFiles) {
|
|
108
|
+
let added = 0;
|
|
109
|
+
let removed = 0;
|
|
110
|
+
let changed = 0;
|
|
111
|
+
let unchanged = 0;
|
|
112
|
+
for (const [name, content] of newFiles) {
|
|
113
|
+
if (!oldFiles.has(name)) added += 1;
|
|
114
|
+
else if (!oldFiles.get(name).equals(content)) changed += 1;
|
|
115
|
+
else unchanged += 1;
|
|
116
|
+
}
|
|
117
|
+
for (const name of oldFiles.keys()) if (!newFiles.has(name)) removed += 1;
|
|
118
|
+
return { added, changed, removed, unchanged, total: newFiles.size };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function replaceSnapshot(nextDir, dataDir) {
|
|
122
|
+
const backupDir = `${dataDir}.cache-backup-${process.pid}`;
|
|
123
|
+
await fs.promises.rm(backupDir, { recursive: true, force: true });
|
|
124
|
+
const hadCurrent = fs.existsSync(dataDir);
|
|
125
|
+
if (hadCurrent) await fs.promises.rename(dataDir, backupDir);
|
|
126
|
+
try {
|
|
127
|
+
await fs.promises.rename(nextDir, dataDir);
|
|
128
|
+
await fs.promises.rm(backupDir, { recursive: true, force: true });
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (!fs.existsSync(dataDir) && hadCurrent && fs.existsSync(backupDir)) await fs.promises.rename(backupDir, dataDir);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function gameSlug(row) {
|
|
136
|
+
const slug = String(row?.game || '').trim().toLowerCase();
|
|
137
|
+
if (!slug || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) throw new Error(`Invalid game slug in code directory: ${JSON.stringify(row?.game)}`);
|
|
138
|
+
return slug;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function validateCodeList(game, payload) {
|
|
142
|
+
if (!payload || Array.isArray(payload) || typeof payload !== 'object') throw new Error(`Invalid code payload for ${game}`);
|
|
143
|
+
if (!Array.isArray(payload.Active) || !Array.isArray(payload.Expired)) throw new Error(`Code payload for ${game} is missing Active/Expired arrays`);
|
|
144
|
+
if (payload.Unknown !== undefined && !Array.isArray(payload.Unknown)) throw new Error(`Unknown codes for ${game} must be an array`);
|
|
145
|
+
return payload;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function stableDirectorySnapshot(directory) {
|
|
149
|
+
const stable = JSON.parse(JSON.stringify(directory));
|
|
150
|
+
const updatedTimes = (stable.games || [])
|
|
151
|
+
.map((game) => new Date(game?.lastUpdatedAt || 0).getTime())
|
|
152
|
+
.filter((value) => Number.isFinite(value) && value > 0);
|
|
153
|
+
|
|
154
|
+
// APINODE's root `updatedAt` describes when the HTTP directory response was
|
|
155
|
+
// generated, so it changes every request. Keep a meaningful stable timestamp
|
|
156
|
+
// based on actual per-game data updates instead.
|
|
157
|
+
if (updatedTimes.length) stable.updatedAt = new Date(Math.max(...updatedTimes)).toISOString();
|
|
158
|
+
else delete stable.updatedAt;
|
|
159
|
+
return stable;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function stableCodeRow(row) {
|
|
163
|
+
const stable = JSON.parse(JSON.stringify(row));
|
|
164
|
+
if (stable.expiry && typeof stable.expiry === 'object' && !Array.isArray(stable.expiry)) {
|
|
165
|
+
// These values are calculated from Date.now() on every API request. The
|
|
166
|
+
// fallback retains expiresAt/local/expired, while consumers can calculate
|
|
167
|
+
// a live countdown themselves when needed.
|
|
168
|
+
delete stable.expiry.expiresInSeconds;
|
|
169
|
+
delete stable.expiry.countdown;
|
|
170
|
+
}
|
|
171
|
+
return stable;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function stableCodePayload(payload) {
|
|
175
|
+
const stable = {};
|
|
176
|
+
for (const bucket of ['Active', 'Expired', 'Unknown']) {
|
|
177
|
+
if (Array.isArray(payload[bucket])) stable[bucket] = payload[bucket].map(stableCodeRow);
|
|
178
|
+
}
|
|
179
|
+
return stable;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function buildNextSnapshot(options) {
|
|
183
|
+
const { apiBase, apiPath, dataDir } = options;
|
|
184
|
+
const nextDir = path.join(path.dirname(dataDir), `.game-codes-cache-next-${process.pid}-${Date.now()}`);
|
|
185
|
+
await fs.promises.rm(nextDir, { recursive: true, force: true });
|
|
186
|
+
await fs.promises.mkdir(nextDir, { recursive: true });
|
|
187
|
+
|
|
188
|
+
const rawDirectory = await requestJson(apiUrl(apiBase, apiPath), options);
|
|
189
|
+
if (!Array.isArray(rawDirectory.games) || !rawDirectory.games.length) {
|
|
190
|
+
throw new Error('Game code directory returned no games; refusing to replace fallback cache');
|
|
191
|
+
}
|
|
192
|
+
const directory = stableDirectorySnapshot(rawDirectory);
|
|
193
|
+
await writeJson(path.join(nextDir, 'directory.json'), directory);
|
|
194
|
+
|
|
195
|
+
// /status is cache-only on APINODE and does not trigger source website requests.
|
|
196
|
+
const statusResponse = await requestJson(apiUrl(apiBase, apiPath, '/status'), options);
|
|
197
|
+
const status = statusResponse.data !== undefined ? statusResponse.data : statusResponse;
|
|
198
|
+
if (!status || typeof status !== 'object') throw new Error('Invalid game code status payload');
|
|
199
|
+
await writeJson(path.join(nextDir, 'status.json'), status);
|
|
200
|
+
|
|
201
|
+
const counts = {};
|
|
202
|
+
const delayMs = numberOption(options.delayMs ?? process.env.GAME_CODES_CACHE_GAME_DELAY_MS, 150, 0, 10000);
|
|
203
|
+
for (let i = 0; i < directory.games.length; i += 1) {
|
|
204
|
+
const game = gameSlug(directory.games[i]);
|
|
205
|
+
const rawPayload = validateCodeList(
|
|
206
|
+
game,
|
|
207
|
+
await requestJson(apiUrl(apiBase, apiPath, `/${encodeURIComponent(game)}`, { includeUnknown: 'true' }), options)
|
|
208
|
+
);
|
|
209
|
+
const payload = stableCodePayload(rawPayload);
|
|
210
|
+
await writeJson(path.join(nextDir, 'games', `${game}.json`), payload);
|
|
211
|
+
counts[game] = {
|
|
212
|
+
active: payload.Active.length,
|
|
213
|
+
expired: payload.Expired.length,
|
|
214
|
+
unknown: Array.isArray(payload.Unknown) ? payload.Unknown.length : 0,
|
|
215
|
+
total: payload.Active.length + payload.Expired.length + (Array.isArray(payload.Unknown) ? payload.Unknown.length : 0)
|
|
216
|
+
};
|
|
217
|
+
if (i < directory.games.length - 1 && delayMs) await sleep(delayMs);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { nextDir, directory, counts };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function syncGameCodeCache(options = {}) {
|
|
224
|
+
if (typeof fetch !== 'function') throw new Error('Node.js 18+ is required because this sync uses the built-in fetch API');
|
|
225
|
+
|
|
226
|
+
const dataDir = path.resolve(options.dataDir || process.env.GAME_CODES_CACHE_DIR || DEFAULT_DATA_DIR);
|
|
227
|
+
const apiBase = options.apiBase || process.env.GAMING_INFOS_API_BASE || DEFAULT_API_BASE;
|
|
228
|
+
const apiPath = options.apiPath || process.env.GAME_CODES_API_PATH || DEFAULT_API_PATH;
|
|
229
|
+
let nextDir;
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const built = await buildNextSnapshot({ ...options, dataDir, apiBase, apiPath });
|
|
233
|
+
nextDir = built.nextDir;
|
|
234
|
+
const oldFiles = await walkFiles(dataDir);
|
|
235
|
+
const newFiles = await walkFiles(nextDir);
|
|
236
|
+
const oldHash = hashFiles(oldFiles);
|
|
237
|
+
const newHash = hashFiles(newFiles);
|
|
238
|
+
const stats = diffStats(oldFiles, newFiles);
|
|
239
|
+
|
|
240
|
+
if (oldFiles.size && oldHash === newHash) {
|
|
241
|
+
await fs.promises.rm(nextDir, { recursive: true, force: true });
|
|
242
|
+
return { success: true, changed: false, contentHash: newHash, source: apiUrl(apiBase, apiPath), counts: built.counts, stats };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const manifest = {
|
|
246
|
+
schemaVersion: 1,
|
|
247
|
+
source: apiUrl(apiBase, apiPath),
|
|
248
|
+
syncedAt: new Date().toISOString(),
|
|
249
|
+
contentHash: newHash,
|
|
250
|
+
gameCount: built.directory.games.length,
|
|
251
|
+
counts: built.counts,
|
|
252
|
+
statusIsCacheOnly: true,
|
|
253
|
+
volatileFieldsRemoved: ['directory.updatedAt(request-time)', 'expiry.expiresInSeconds', 'expiry.countdown'],
|
|
254
|
+
stats
|
|
255
|
+
};
|
|
256
|
+
await writeJson(path.join(nextDir, '.cache-manifest.json'), manifest);
|
|
257
|
+
await replaceSnapshot(nextDir, dataDir);
|
|
258
|
+
nextDir = null;
|
|
259
|
+
return { success: true, changed: true, contentHash: newHash, source: manifest.source, counts: built.counts, stats };
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (nextDir) await fs.promises.rm(nextDir, { recursive: true, force: true }).catch(() => {});
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function main() {
|
|
267
|
+
const result = await syncGameCodeCache();
|
|
268
|
+
console.log(JSON.stringify(result, null, 2));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (require.main === module) {
|
|
272
|
+
main().catch((error) => {
|
|
273
|
+
console.error(`[game-codes cache] ${error.stack || error.message || error}`);
|
|
274
|
+
process.exitCode = 1;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = {
|
|
279
|
+
stableDirectorySnapshot,
|
|
280
|
+
stableCodePayload,
|
|
281
|
+
syncGameCodeCache,
|
|
282
|
+
validateCodeList
|
|
283
|
+
};
|