commit 69da9f42d470879bcbcba06092bebb4387449426 Author: HorizonCode Date: Fri Aug 1 21:42:02 2025 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/2637354/108051.osr b/2637354/108051.osr new file mode 100644 index 0000000..316c8ca Binary files /dev/null and b/2637354/108051.osr differ diff --git a/2637354/3175608.osr b/2637354/3175608.osr new file mode 100644 index 0000000..bd9fc7e Binary files /dev/null and b/2637354/3175608.osr differ diff --git a/2637354/322849.osr b/2637354/322849.osr new file mode 100644 index 0000000..af43e36 Binary files /dev/null and b/2637354/322849.osr differ diff --git a/2637354/3579210.osr b/2637354/3579210.osr new file mode 100644 index 0000000..4613435 Binary files /dev/null and b/2637354/3579210.osr differ diff --git a/2637354/3712168.osr b/2637354/3712168.osr new file mode 100644 index 0000000..b0c8092 Binary files /dev/null and b/2637354/3712168.osr differ diff --git a/2637354/3823168.osr b/2637354/3823168.osr new file mode 100644 index 0000000..eee0bb2 Binary files /dev/null and b/2637354/3823168.osr differ diff --git a/2637354/4274784.osr b/2637354/4274784.osr new file mode 100644 index 0000000..b7fafeb Binary files /dev/null and b/2637354/4274784.osr differ diff --git a/2637354/4331683.osr b/2637354/4331683.osr new file mode 100644 index 0000000..18b936c Binary files /dev/null and b/2637354/4331683.osr differ diff --git a/2637354/4393946.osr b/2637354/4393946.osr new file mode 100644 index 0000000..85aa818 Binary files /dev/null and b/2637354/4393946.osr differ diff --git a/2637354/459579.osr b/2637354/459579.osr new file mode 100644 index 0000000..75c79e9 Binary files /dev/null and b/2637354/459579.osr differ diff --git a/2637354/4811455.osr b/2637354/4811455.osr new file mode 100644 index 0000000..b928db6 Binary files /dev/null and b/2637354/4811455.osr differ diff --git a/2637354/4934795.osr b/2637354/4934795.osr new file mode 100644 index 0000000..c9f5ffa Binary files /dev/null and b/2637354/4934795.osr differ diff --git a/2637354/607421.osr b/2637354/607421.osr new file mode 100644 index 0000000..f4c949a Binary files /dev/null and b/2637354/607421.osr differ diff --git a/2637354/75690.osr b/2637354/75690.osr new file mode 100644 index 0000000..5859e2c Binary files /dev/null and b/2637354/75690.osr differ diff --git a/2637354/89835.osr b/2637354/89835.osr new file mode 100644 index 0000000..d24dfed Binary files /dev/null and b/2637354/89835.osr differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..81eb034 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# replay_downloader + +A Tool to mass download EZPPFarm replays from specific leaderboards + +## Dependencies + +- [Bun](https://bun.sh) + +## To install bun: + +### Windows: + +```bash +powershell -c "irm bun.sh/install.ps1 | iex" +``` + +### Linux & macOS: + +```bash +curl -fsSL https://bun.sh/install | bash +``` + +--- + +## To install dependencies: + +```bash +bun install +``` + +--- + +## To debug: + +```bash +bun run debug +``` + +--- + +## To build: + +```bash +bun run build +``` diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..a8e0a95 --- /dev/null +++ b/bun.lock @@ -0,0 +1,158 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "replay_downloader", + "dependencies": { + "replay_downloader": ".", + "@better-fetch/fetch": "^1.1.18", + "chalk": "^5.4.1", + "inquirer": "^12.9.0", + "ky": "^1.8.2", + "ora": "^8.2.0", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/chalk": "^2.2.4", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="], + + "@inquirer/checkbox": ["@inquirer/checkbox@4.2.0", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA=="], + + "@inquirer/confirm": ["@inquirer/confirm@5.1.14", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q=="], + + "@inquirer/core": ["@inquirer/core@10.1.15", "", { "dependencies": { "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA=="], + + "@inquirer/editor": ["@inquirer/editor@4.2.15", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8", "external-editor": "^3.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-wst31XT8DnGOSS4nNJDIklGKnf+8shuauVrWzgKegWUe28zfCftcWZ2vktGdzJgcylWSS2SrDnYUb6alZcwnCQ=="], + + "@inquirer/expand": ["@inquirer/expand@4.0.17", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw=="], + + "@inquirer/figures": ["@inquirer/figures@1.0.13", "", {}, "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw=="], + + "@inquirer/input": ["@inquirer/input@4.2.1", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow=="], + + "@inquirer/number": ["@inquirer/number@3.0.17", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg=="], + + "@inquirer/password": ["@inquirer/password@4.0.17", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA=="], + + "@inquirer/prompts": ["@inquirer/prompts@7.8.0", "", { "dependencies": { "@inquirer/checkbox": "^4.2.0", "@inquirer/confirm": "^5.1.14", "@inquirer/editor": "^4.2.15", "@inquirer/expand": "^4.0.17", "@inquirer/input": "^4.2.1", "@inquirer/number": "^3.0.17", "@inquirer/password": "^4.0.17", "@inquirer/rawlist": "^4.1.5", "@inquirer/search": "^3.1.0", "@inquirer/select": "^4.3.1" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-JHwGbQ6wjf1dxxnalDYpZwZxUEosT+6CPGD9Zh4sm9WXdtUp9XODCQD3NjSTmu+0OAyxWXNOqf0spjIymJa2Tw=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@4.1.5", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA=="], + + "@inquirer/search": ["@inquirer/search@3.1.0", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PMk1+O/WBcYJDq2H7foV0aAZSmDdkzZB9Mw2v/DmONRJopwA/128cS9M/TXWLKKdEQKZnKwBzqu2G4x/2Nqx8Q=="], + + "@inquirer/select": ["@inquirer/select@4.3.1", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA=="], + + "@inquirer/type": ["@inquirer/type@3.0.8", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw=="], + + "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], + + "@types/chalk": ["@types/chalk@2.2.4", "", { "dependencies": { "chalk": "*" } }, "sha512-pb/QoGqtCpH2famSp72qEsXkNzcErlVmiXlQ/ww+5AddD8TmmYS7EWg5T20YiNCAiTgs8pMf2G8SJG5h/ER1ZQ=="], + + "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], + + "@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="], + + "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], + + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + + "chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], + + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + + "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="], + + "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "inquirer": ["inquirer@12.9.0", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/prompts": "^7.8.0", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "mute-stream": "^2.0.0", "run-async": "^4.0.5", "rxjs": "^7.8.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-LlFVmvWVCun7uEgPB3vups9NzBrjJn48kRNtFGw3xU1H5UXExTEz/oF1JGLaB0fvlkUB+W6JfgLcSEaSdH7RPA=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "ky": ["ky@1.8.2", "", {}, "sha512-XybQJ3d4Ea1kI27DoelE5ZCT3bSJlibYTtQuMsyzKox3TMyayw1asgQdl54WroAm+fIA3ZCr8zXW2RpR7qWVpA=="], + + "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], + + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], + + "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], + + "replay_downloader": ["replay_downloader@file:", { "dependencies": { "@better-fetch/fetch": "^1.1.18", "chalk": "^5.4.1", "inquirer": "^12.9.0", "ky": "^1.8.2", "ora": "^8.2.0" }, "devDependencies": { "@types/bun": "latest", "@types/chalk": "^2.2.4" }, "peerDependencies": { "typescript": "^5" } }], + + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "run-async": ["run-async@4.0.5", "", {}, "sha512-oN9GTgxUNDBumHTTDmQ8dep6VIJbgj9S3dPP+9XylVLIK4xB9XTXtKWROd5pnhdXR9k0EgO1JRcNh0T+Ny2FsA=="], + + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], + + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="], + + "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], + + "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..de613c0 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "replay_downloader", + "module": "src/index.ts", + "type": "module", + "private": true, + "scripts": { + "debug": "bun --watch src/index.ts", + "build": "bun run build:win && bun run build:linux && bun run build:mac", + "build:win": "bun build ./src/index.ts --compile --target=bun-windows-x64-modern --outfile ./target/replay_downloader-windows-x64", + "build:linux": "bun build ./src/index.ts --compile --target=bun-linux-x64-modern --outfile ./target/replay_downloader-linux-x64", + "build:mac": "bun build ./src/index.ts --compile --target=bun-darwin-x64 --outfile ./target/replay_downloader-darwin-x64" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/chalk": "^2.2.4" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@better-fetch/fetch": "^1.1.18", + "chalk": "^5.4.1", + "inquirer": "^12.9.0", + "ky": "^1.8.2", + "ora": "^8.2.0" + } +} diff --git a/src/api/ezpp.ts b/src/api/ezpp.ts new file mode 100644 index 0000000..6ff8c55 --- /dev/null +++ b/src/api/ezpp.ts @@ -0,0 +1,37 @@ +import { type Method, createFetch } from '@better-fetch/fetch'; +import type { HeadersInit } from 'bun'; + +const apiClient = createFetch({ + baseURL: 'https://api.ez-pp.farm/', + throw: true, +}); + +export type ApiResponse = { data: T; error: undefined } | { data: undefined; error: Error }; + +export const fetchApi = async ( + route: string, + options?: { + query?: Record; + headers?: HeadersInit; + method?: Method; + params?: Record; + timeout?: number; + signal?: AbortSignal; + } +): Promise> => { + try { + const data = await apiClient(route, { + query: options?.query, + headers: options?.headers ?? { + accept: 'application/json', + }, + method: options?.method ?? 'GET', + params: options?.params, + timeout: options?.timeout, + signal: options?.signal, + }); + return { data, error: undefined }; + } catch (err) { + return { data: undefined, error: err as Error }; + } +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..002159e --- /dev/null +++ b/src/index.ts @@ -0,0 +1,108 @@ +import inquirer from "inquirer"; +import { fetchApi } from "./api/ezpp"; +import ora from "ora"; +import { + getGamemodeName, + getModeAndTypeFromGamemode, + modeIntToStr, + typeIntToStr, + validModeTypeCombinationsSorted, +} from "./util/mode"; +import type { MapScores } from "./interface/MapScores"; +import * as path from "node:path"; +import { betterFetch } from "@better-fetch/fetch"; +import ky from "ky"; +import { mkdir } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import chalk from "chalk"; + +(async () => { + const modeChoices = validModeTypeCombinationsSorted.map((gamemode) => { + const gmode = getModeAndTypeFromGamemode(gamemode); + return { + value: gamemode, + name: getGamemodeName(modeIntToStr(gmode.mode), typeIntToStr(gmode.type)), + }; + }); + + const answers: { beatmapId: number; mode: number } = await inquirer.prompt([ + { + message: "Please enter a beatmap ID", + name: "beatmapId", + type: "number", + min: 0, + required: true, + async validate(value) { + const { error } = await fetchApi("v1/get_map_info", { + query: { + id: value, + }, + }); + if (error) return "Beatmap not found!"; + return true; + }, + }, + { + type: "select", + name: "mode", + message: "Please select a mode", + choices: modeChoices, + loop: false, + }, + ]); + + const leaderboardLoadingSpinner = ora("Loading scores...").start(); + + const { error, data } = await fetchApi("v1/get_map_scores", { + query: { + id: answers.beatmapId, + mode: answers.mode, + scope: "best", + limit: 50, + }, + }); + + if (error || !data.scores || data.scores.length <= 0) { + leaderboardLoadingSpinner.fail("Failed to get map scores."); + process.exit(0); + } + + leaderboardLoadingSpinner.succeed("Scores loaded!"); + + const currentFolder = path.join(process.cwd()); + const downloadFolder = path.join(currentFolder, answers.beatmapId.toFixed()); + if (!existsSync(downloadFolder)) await mkdir(downloadFolder); + + let downloadedReplaysCount = 0; + + for (const score of data.scores) { + const downloadSpinner = ora( + `downloading replay ${score.id} (0%)...` + ).start(); + try { + const replayDownload = await ky( + `https://api.ez-pp.farm/v1/get_replay?id=${score.id}&include_headers=true`, + { + onDownloadProgress(progress) { + downloadSpinner.text = `downloading replay ${score.id} (${(progress.percent * 100).toFixed()}%)...`; + }, + } + ); + const replayData = await replayDownload.arrayBuffer(); + downloadSpinner.text = `Saving replay ${score.id}...`; + await Bun.write( + path.join(downloadFolder, `${score.id}.osr`), + Buffer.from(replayData) + ); + downloadedReplaysCount += 1; + } catch { + downloadSpinner.fail(`Failed to download replay ${score.id}.`); + } + if (downloadSpinner.isSpinning) { + downloadSpinner.succeed(`Saved replay ${score.id}!`); + } + await new Promise((res) => setTimeout(res, 150)); + } + console.log(`${chalk.green("✔")} Downloaded ${downloadedReplaysCount}/${data.scores.length} replay(s)!`); + process.exit(0); +})(); diff --git a/src/interface/MapScores.ts b/src/interface/MapScores.ts new file mode 100644 index 0000000..acb4063 --- /dev/null +++ b/src/interface/MapScores.ts @@ -0,0 +1,34 @@ +export type MapScores = { + status: string; + scores: { + id: number; + map_md5: string; + score: number; + pp: number; + acc: number; + max_combo: number; + mods: number; + n300: number; + n100: number; + n50: number; + nmiss: number; + ngeki: number; + nkatu: number; + grade: string; + status: number; + mode: number; + play_time: string; + time_elapsed: number; + userid: number; + perfect: number; + player_name: string; + country: string; + username_style: string; + priv: number; + creation_time: number; + latest_activity: number; + clan_id: number | null; + clan_name: string | null; + clan_tag: string | null; + }[]; +}; diff --git a/src/util/mode.ts b/src/util/mode.ts new file mode 100644 index 0000000..efb93fc --- /dev/null +++ b/src/util/mode.ts @@ -0,0 +1,186 @@ +export enum Gamemodes { + VANILLA_OSU = 0, + VANILLA_TAIKO = 1, + VANILLA_CATCH = 2, + VANILLA_MANIA = 3, + + RELAX_OSU = 4, + RELAX_TAIKO = 5, + RELAX_CATCH = 6, + + AUTOPILOT_OSU = 8, +} + +export enum Mode { + OSU = 0, + TAIKO = 1, + CATCH = 2, + MANIA = 3, +} + +export enum Type { + VANILLA = 0, + RELAX = 4, + AUTOPILOT = 8, +} + +export const validModes = [Mode.OSU, Mode.TAIKO, Mode.CATCH, Mode.MANIA]; +export const validTypes = [Type.VANILLA, Type.RELAX, Type.AUTOPILOT]; +export const validModeTypeCombinations = [0, 1, 2, 3, 4, 5, 6, 8]; +export const validModeTypeCombinationsSorted = [0, 4, 8, 1, 5, 2, 6, 3]; + +export const validMode = (modeStr: string) => modeStrToInt(modeStr) !== undefined; +export const validType = (typeStr: string) => typeStrToInt(typeStr) !== undefined; + +export const modeStrToInt = (modeStr: 'osu' | 'taiko' | 'catch' | 'mania' | string) => { + switch (modeStr) { + case 'taiko': + return Mode.TAIKO; + case 'catch': + return Mode.CATCH; + case 'mania': + return Mode.MANIA; + case 'osu': + return Mode.OSU; + } + return undefined; +}; + +export const modeIntToStr = (modeInt: number) => { + switch (modeInt) { + case Mode.TAIKO: + return 'taiko'; + case Mode.CATCH: + return 'catch'; + case Mode.MANIA: + return 'mania'; + case Mode.OSU: + return 'osu'; + } + return undefined; +}; + +export const typeStrToInt = (typeStr: 'vanilla' | 'relax' | 'autopilot' | string) => { + switch (typeStr) { + case 'relax': + return Type.RELAX; + case 'autopilot': + return Type.AUTOPILOT; + case 'vanilla': + return Type.VANILLA; + } + return undefined; +}; + +export const typeIntToStr = (typeInt: number) => { + switch (typeInt) { + case Type.RELAX: + return 'relax'; + case Type.AUTOPILOT: + return 'autopilot'; + case Type.VANILLA: + return 'vanilla'; + } + return undefined; +}; + +export const getGamemodeInt = ( + mode: 'osu' | 'taiko' | 'catch' | 'mania' | string | undefined, + type: 'vanilla' | 'relax' | 'autopilot' | string | undefined +) => { + let modee = 0; + switch (mode) { + case 'taiko': + modee += Mode.TAIKO; + break; + case 'catch': + modee += Mode.CATCH; + break; + case 'mania': + modee += Mode.MANIA; + break; + } + + switch (type) { + case 'relax': + modee += Type.RELAX; + break; + case 'autopilot': + modee += Type.AUTOPILOT; + break; + } + + return modee; +}; + +export const getGamemodeName = ( + mode: 'osu' | 'taiko' | 'catch' | 'mania' | string | undefined, + type: 'vanilla' | 'relax' | 'autopilot' | string | undefined +) => { + let modeStr = ''; + switch (mode) { + case 'taiko': + modeStr += 'taiko!'; + break; + case 'catch': + modeStr += 'catch!'; + break; + case 'mania': + modeStr += 'mania!'; + break; + default: + modeStr += 'osu!'; + break; + } + + switch (type) { + case 'relax': + modeStr += 'rx'; + break; + case 'autopilot': + modeStr += 'ap'; + break; + default: + modeStr += 'vn'; + break; + } + + return modeStr; +}; + +export const getModeAndTypeFromGamemode = (gamemode: number) => { + let mode = Mode.OSU; + let type = Type.VANILLA; + const vanillaMode = gamemode % 4; + + switch (vanillaMode) { + case Mode.TAIKO: + mode = Mode.TAIKO; + break; + case Mode.CATCH: + mode = Mode.CATCH; + break; + case Mode.MANIA: + mode = Mode.MANIA; + break; + } + + const typee = gamemode - vanillaMode; + switch (typee) { + case Type.RELAX: + type = Type.RELAX; + break; + case Type.AUTOPILOT: + type = Type.AUTOPILOT; + break; + } + + return { + mode, + type, + }; +}; + +export const isValidGamemode = (gamemodeInt: number) => { + return validModeTypeCombinations.includes(gamemodeInt); +}; \ No newline at end of file diff --git a/target b/target new file mode 160000 index 0000000..13feb37 --- /dev/null +++ b/target @@ -0,0 +1 @@ +Subproject commit 13feb373dc14ca78406fa79bfd7b53b7c982ef80 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}