diff --git a/package.json b/package.json index 6eb0623..d102a1f 100644 --- a/package.json +++ b/package.json @@ -15,14 +15,18 @@ "lint": "next lint" }, "dependencies": { + "axios": "^1.6.8", "next": "14.1.4", + "pino": "^8.19.0", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "user-agents": "^1.1.156" }, "devDependencies": { "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@types/user-agents": "^1.0.4", "autoprefixer": "^10.0.1", "eslint": "^8", "eslint-config-next": "14.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 17d8c56..18c3328 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,15 +5,24 @@ settings: excludeLinksFromLockfile: false dependencies: + axios: + specifier: ^1.6.8 + version: 1.6.8 next: specifier: 14.1.4 version: 14.1.4(react-dom@18.2.0)(react@18.2.0) + pino: + specifier: ^8.19.0 + version: 8.19.0 react: specifier: ^18 version: 18.2.0 react-dom: specifier: ^18 version: 18.2.0(react@18.2.0) + user-agents: + specifier: ^1.1.156 + version: 1.1.156 devDependencies: '@types/node': @@ -25,6 +34,9 @@ devDependencies: '@types/react-dom': specifier: ^18 version: 18.2.22 + '@types/user-agents': + specifier: ^1.0.4 + version: 1.0.4 autoprefixer: specifier: ^10.0.1 version: 10.4.19(postcss@8.4.38) @@ -322,6 +334,10 @@ packages: csstype: 3.1.3 dev: true + /@types/user-agents@1.0.4: + resolution: {integrity: sha512-AjeFc4oX5WPPflgKfRWWJfkEk7Wu82fnj1rROPsiqFt6yElpdGFg8Srtm/4PU4rA9UiDUZlruGPgcwTMQlwq4w==, tarball: https://registry.npmmirror.com/@types/user-agents/-/user-agents-1.0.4.tgz} + dev: true + /@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3): resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==, tarball: https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-6.21.0.tgz} engines: {node: ^16.0.0 || >=18.0.0} @@ -390,6 +406,13 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==, tarball: https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz} dev: true + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==, tarball: https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: false + /acorn-jsx@5.3.2(acorn@8.11.3): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, tarball: https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz} peerDependencies: @@ -567,6 +590,15 @@ packages: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==, tarball: https://registry.npmmirror.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz} dev: true + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==, tarball: https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz} + dev: false + + /atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==, tarball: https://registry.npmmirror.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz} + engines: {node: '>=8.0.0'} + dev: false + /autoprefixer@10.4.19(postcss@8.4.38): resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==, tarball: https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.19.tgz} engines: {node: ^10 || ^12 || >=14} @@ -595,6 +627,16 @@ packages: engines: {node: '>=4'} dev: true + /axios@1.6.8: + resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==, tarball: https://registry.npmmirror.com/axios/-/axios-1.6.8.tgz} + dependencies: + follow-redirects: 1.15.6 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==, tarball: https://registry.npmmirror.com/axobject-query/-/axobject-query-3.2.1.tgz} dependencies: @@ -605,6 +647,10 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, tarball: https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz} dev: true + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, tarball: https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz} + dev: false + /binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==, tarball: https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz} engines: {node: '>=8'} @@ -641,6 +687,13 @@ packages: update-browserslist-db: 1.0.13(browserslist@4.23.0) dev: true + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==, tarball: https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + /busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==, tarball: https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz} engines: {node: '>=10.16.0'} @@ -710,6 +763,13 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, tarball: https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz} dev: true + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==, tarball: https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==, tarball: https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz} engines: {node: '>= 6'} @@ -814,6 +874,11 @@ packages: object-keys: 1.1.1 dev: true + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==, tarball: https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz} + engines: {node: '>=0.4.0'} + dev: false + /dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==, tarball: https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz} engines: {node: '>=6'} @@ -1274,6 +1339,16 @@ packages: engines: {node: '>=0.10.0'} dev: true + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==, tarball: https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz} + engines: {node: '>=6'} + dev: false + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==, tarball: https://registry.npmmirror.com/events/-/events-3.3.0.tgz} + engines: {node: '>=0.8.x'} + dev: false + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, tarball: https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz} dev: true @@ -1297,6 +1372,11 @@ packages: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, tarball: https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz} dev: true + /fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==, tarball: https://registry.npmmirror.com/fast-redact/-/fast-redact-3.5.0.tgz} + engines: {node: '>=6'} + dev: false + /fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==, tarball: https://registry.npmmirror.com/fastq/-/fastq-1.17.1.tgz} dependencies: @@ -1338,6 +1418,16 @@ packages: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==, tarball: https://registry.npmmirror.com/flatted/-/flatted-3.3.1.tgz} dev: true + /follow-redirects@1.15.6: + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==, tarball: https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.6.tgz} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==, tarball: https://registry.npmmirror.com/for-each/-/for-each-0.3.3.tgz} dependencies: @@ -1352,6 +1442,15 @@ packages: signal-exit: 4.1.0 dev: true + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==, tarball: https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + /fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==, tarball: https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz} dev: true @@ -1527,6 +1626,10 @@ packages: function-bind: 1.1.2 dev: true + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==, tarball: https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz} + dev: false + /ignore@5.3.1: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==, tarball: https://registry.npmmirror.com/ignore/-/ignore-5.3.1.tgz} engines: {node: '>= 4'} @@ -1860,6 +1963,10 @@ packages: p-locate: 5.0.0 dev: true + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==, tarball: https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz} + dev: false + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==, tarball: https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz} dev: true @@ -1895,6 +2002,18 @@ packages: picomatch: 2.3.1 dev: true + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==, tarball: https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==, tarball: https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==, tarball: https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz} dependencies: @@ -2070,6 +2189,11 @@ packages: es-object-atoms: 1.0.0 dev: true + /on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==, tarball: https://registry.npmmirror.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz} + engines: {node: '>=14.0.0'} + dev: false + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, tarball: https://registry.npmmirror.com/once/-/once-1.4.0.tgz} dependencies: @@ -2154,6 +2278,34 @@ packages: engines: {node: '>=0.10.0'} dev: true + /pino-abstract-transport@1.1.0: + resolution: {integrity: sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==, tarball: https://registry.npmmirror.com/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz} + dependencies: + readable-stream: 4.5.2 + split2: 4.2.0 + dev: false + + /pino-std-serializers@6.2.2: + resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==, tarball: https://registry.npmmirror.com/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz} + dev: false + + /pino@8.19.0: + resolution: {integrity: sha512-oswmokxkav9bADfJ2ifrvfHUwad6MLp73Uat0IkQWY3iAw5xTRoznXbXksZs8oaOUMpmhVWD+PZogNzllWpJaA==, tarball: https://registry.npmmirror.com/pino/-/pino-8.19.0.tgz} + hasBin: true + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 1.1.0 + pino-std-serializers: 6.2.2 + process-warning: 3.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.4.3 + sonic-boom: 3.8.0 + thread-stream: 2.4.1 + dev: false + /pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==, tarball: https://registry.npmmirror.com/pirates/-/pirates-4.0.6.tgz} engines: {node: '>= 6'} @@ -2248,6 +2400,15 @@ packages: engines: {node: '>= 0.8.0'} dev: true + /process-warning@3.0.0: + resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==, tarball: https://registry.npmmirror.com/process-warning/-/process-warning-3.0.0.tgz} + dev: false + + /process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==, tarball: https://registry.npmmirror.com/process/-/process-0.11.10.tgz} + engines: {node: '>= 0.6.0'} + dev: false + /prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==, tarball: https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz} dependencies: @@ -2256,6 +2417,10 @@ packages: react-is: 16.13.1 dev: true + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==, tarball: https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz} + dev: false + /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, tarball: https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz} engines: {node: '>=6'} @@ -2265,6 +2430,10 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, tarball: https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz} dev: true + /quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==, tarball: https://registry.npmmirror.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz} + dev: false + /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==, tarball: https://registry.npmmirror.com/react-dom/-/react-dom-18.2.0.tgz} peerDependencies: @@ -2292,6 +2461,17 @@ packages: pify: 2.3.0 dev: true + /readable-stream@4.5.2: + resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==, tarball: https://registry.npmmirror.com/readable-stream/-/readable-stream-4.5.2.tgz} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + dev: false + /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==, tarball: https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz} engines: {node: '>=8.10.0'} @@ -2299,6 +2479,11 @@ packages: picomatch: 2.3.1 dev: true + /real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==, tarball: https://registry.npmmirror.com/real-require/-/real-require-0.2.0.tgz} + engines: {node: '>= 12.13.0'} + dev: false + /reflect.getprototypeof@1.0.6: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==, tarball: https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz} engines: {node: '>= 0.4'} @@ -2381,6 +2566,10 @@ packages: isarray: 2.0.5 dev: true + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==, tarball: https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz} + dev: false + /safe-regex-test@1.0.3: resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==, tarball: https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz} engines: {node: '>= 0.4'} @@ -2390,6 +2579,11 @@ packages: is-regex: 1.1.4 dev: true + /safe-stable-stringify@2.4.3: + resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==, tarball: https://registry.npmmirror.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz} + engines: {node: '>=10'} + dev: false + /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==, tarball: https://registry.npmmirror.com/scheduler/-/scheduler-0.23.0.tgz} dependencies: @@ -2463,10 +2657,21 @@ packages: engines: {node: '>=8'} dev: true + /sonic-boom@3.8.0: + resolution: {integrity: sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==, tarball: https://registry.npmmirror.com/sonic-boom/-/sonic-boom-3.8.0.tgz} + dependencies: + atomic-sleep: 1.0.0 + dev: false + /source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==, tarball: https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.0.tgz} engines: {node: '>=0.10.0'} + /split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==, tarball: https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz} + engines: {node: '>= 10.x'} + dev: false + /streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==, tarball: https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz} engines: {node: '>=10.0.0'} @@ -2535,6 +2740,12 @@ packages: es-object-atoms: 1.0.0 dev: true + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==, tarball: https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz} + dependencies: + safe-buffer: 5.2.1 + dev: false + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, tarball: https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz} engines: {node: '>=8'} @@ -2655,6 +2866,12 @@ packages: any-promise: 1.3.0 dev: true + /thread-stream@2.4.1: + resolution: {integrity: sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==, tarball: https://registry.npmmirror.com/thread-stream/-/thread-stream-2.4.1.tgz} + dependencies: + real-require: 0.2.0 + dev: false + /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==, tarball: https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz} engines: {node: '>=8.0'} @@ -2780,6 +2997,12 @@ packages: punycode: 2.3.1 dev: true + /user-agents@1.1.156: + resolution: {integrity: sha512-gphkDF41zQ/JGHc4hv2n05/okucmb7qYlhN2BKur0IKd1pkgXhS6l1GILoMQ/Hak7Z5FP3+X+0h7QcGF/pgxXw==, tarball: https://registry.npmmirror.com/user-agents/-/user-agents-1.1.156.tgz} + dependencies: + lodash.clonedeep: 4.5.0 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, tarball: https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz} dev: true diff --git a/src/api/custom_generate/route.ts b/src/api/custom_generate/route.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/api/generate/route.ts b/src/api/generate/route.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/api/get/route.ts b/src/api/get/route.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/api/get_limit/route.ts b/src/api/get_limit/route.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/api/custom_generate/route.ts b/src/app/api/custom_generate/route.ts new file mode 100644 index 0000000..e73e6df --- /dev/null +++ b/src/app/api/custom_generate/route.ts @@ -0,0 +1,50 @@ +import { NextResponse, NextRequest } from "next/server"; +import SunoApi from '@/lib/sunoApi'; + +export async function POST(req: NextRequest) { + if (req.method === 'POST') { + try { + const body = await req.json(); + const { prompt, tags, title, make_instrumental, wait_audio } = body; + + // 校验输入参数 + if (!prompt || !tags || !title) { + return new NextResponse(JSON.stringify({ error: 'Prompt, tags, and title are required' }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + // 调用 SunoApi.custom_generate 方法生成定制音频 + const audioInfo = await SunoApi.custom_generate( + prompt, tags, title, + make_instrumental == true, + wait_audio == true + ); + + // 使用 NextResponse 构建成功响应 + return new NextResponse(JSON.stringify(audioInfo), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } catch (error: any) { + console.error('Error generating custom audio:', error.response.data); + if (error.response.status === 402) { + return new NextResponse(JSON.stringify({ error: error.response.data.detail }), { + status: 402, + headers: { 'Content-Type': 'application/json' } + }); + } + // 使用 NextResponse 构建错误响应 + return new NextResponse(JSON.stringify({ error: 'Internal server error' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } + } else { + return new NextResponse('Method Not Allowed', { + headers: { Allow: 'POST' }, + status: 405 + }); + } +} \ No newline at end of file diff --git a/src/app/api/generate/route.ts b/src/app/api/generate/route.ts new file mode 100644 index 0000000..77584de --- /dev/null +++ b/src/app/api/generate/route.ts @@ -0,0 +1,46 @@ +import { NextResponse, NextRequest } from "next/server"; +import SunoApi from '@/lib/sunoApi'; + +export async function POST(req: NextRequest) { + if (req.method === 'POST') { + try { + const body = await req.json(); + const { prompt, make_instrumental, wait_audio } = body; + + // 校验输入参数 + if (!prompt) { + return new NextResponse(JSON.stringify({ error: 'Prompt is required' }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + // 调用 SunoApi.generate 方法生成音频 + const audioInfo = await SunoApi.generate(prompt, make_instrumental == true, wait_audio == true); + + // 使用 NextResponse 构建成功响应 + return new NextResponse(JSON.stringify(audioInfo), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } catch (error: any) { + console.error('Error generating custom audio:', JSON.stringify(error.response.data)); + if (error.response.status === 402) { + return new NextResponse(JSON.stringify({ error: error.response.data.detail }), { + status: 402, + headers: { 'Content-Type': 'application/json' } + }); + } + // 使用 NextResponse 构建错误响应 + return new NextResponse(JSON.stringify({ error: 'Internal server error: ' + JSON.stringify(error.response.data.detail) }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } + } else { + return new NextResponse('Method Not Allowed', { + headers: { Allow: 'POST' }, + status: 405 + }); + } +} \ No newline at end of file diff --git a/src/app/api/get/route.ts b/src/app/api/get/route.ts new file mode 100644 index 0000000..3a1d2d7 --- /dev/null +++ b/src/app/api/get/route.ts @@ -0,0 +1,38 @@ +import { NextResponse, NextRequest } from "next/server"; +import SunoApi from '@/lib/sunoApi'; + +export async function GET(req: NextRequest) { + if (req.method === 'GET') { + try { + // 修复了获取查询参数的方式 + const url = new URL(req.url); + const songIds = url.searchParams.get('ids'); + let audioInfo = []; + if (songIds && songIds.length > 0) { + const idsArray = songIds.split(','); + // 调用 SunoApi.get 方法获取音频信息 + audioInfo = await SunoApi.get(idsArray); + } else { + audioInfo = await SunoApi.get(); + } + + // 使用 NextResponse 构建成功响应 + return new NextResponse(JSON.stringify(audioInfo), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } catch (error) { + console.error('Error fetching audio:', error); + // 使用 NextResponse 构建错误响应 + return new NextResponse(JSON.stringify({ error: 'Internal server error' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } + } else { + return new NextResponse('Method Not Allowed', { + headers: { Allow: 'GET' }, + status: 405 + }); + } +} \ No newline at end of file diff --git a/src/app/api/get_limit/route.ts b/src/app/api/get_limit/route.ts new file mode 100644 index 0000000..0bac475 --- /dev/null +++ b/src/app/api/get_limit/route.ts @@ -0,0 +1,29 @@ +import { NextResponse, NextRequest } from "next/server"; +import SunoApi from '@/lib/sunoApi'; + +export async function GET(req: NextRequest) { + if (req.method === 'GET') { + try { + // 调用 SunoApi.get_limit 方法获取剩余的信用额度 + const limit = await SunoApi.get_limit(); + + // 使用 NextResponse 构建成功响应 + return new NextResponse(JSON.stringify({ limit }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } catch (error) { + console.error('Error fetching limit:', error); + // 使用 NextResponse 构建错误响应 + return new NextResponse(JSON.stringify({ error: 'Internal server error' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } + } else { + return new NextResponse('Method Not Allowed', { + headers: { Allow: 'GET' }, + status: 405 + }); + } +} \ No newline at end of file diff --git a/src/lib/sunoApi.ts b/src/lib/sunoApi.ts index e69de29..db14d10 100644 --- a/src/lib/sunoApi.ts +++ b/src/lib/sunoApi.ts @@ -0,0 +1,285 @@ +import axios from 'axios'; +import UserAgent from 'user-agents'; + + + +interface AudioInfo { + id: string; + title?: string; + image_url?: string; + lyric?: string; + audio_url?: string; + video_url?: string; + created_at: string; + model_name: string; + gpt_description_prompt?: string; + prompt?: string; + status: string; + type?: string; + tags?: string; + duration?: string; +} +/** + * 暂停指定的秒数。 + * @param x 最小秒数。 + * @param y 最大秒数(可选)。 + */ +const sleep = (x: number, y?: number): Promise => { + let timeout = x * 1000; + if (y !== undefined && y !== x) { + const min = Math.min(x, y); + const max = Math.max(x, y); + timeout = Math.floor(Math.random() * (max - min + 1) + min) * 1000; + } + console.log(`Sleeping for ${timeout / 1000} seconds`); + return new Promise(resolve => setTimeout(resolve, timeout)); +} + +class SunoApi { + private static baseUrl: string = 'https://studio-api.suno.ai'; + private static clerkBaseUrl: string = 'https://clerk.suno.ai'; + private static cookie: string = process.env.SUNO_COOKIE || ''; + private static userAgent: string = new UserAgent().toString(); + private static sid: string | null = null; + + private static async getAuthToken(): Promise { + + // 获取会话ID的URL + const getSessionUrl = `${SunoApi.clerkBaseUrl}/v1/client?_clerk_js_version=4.70.5`; + // 交换令牌的URL模板 + const exchangeTokenUrlTemplate = `${SunoApi.clerkBaseUrl}/v1/client/sessions/{sid}/tokens/api?_clerk_js_version=4.70.0`; + + // 获取会话ID + const sessionResponse = await axios.get(getSessionUrl, { + headers: { + 'User-Agent': SunoApi.userAgent, + 'Cookie': SunoApi.cookie, + }, + }); + const sid = sessionResponse.data.response?.last_active_session_id; + if (!sid) { + throw new Error("Failed to get session id"); + } + console.log(`Successfully retrieved session ID: ${sid}`); + SunoApi.sid = sid; // 保存会话ID以备后用 + + // 使用会话ID获取JWT令牌 + const exchangeTokenUrl = exchangeTokenUrlTemplate.replace('{sid}', sid); + // console.log("Exchange Token URL:\n", exchangeTokenUrl); + // console.log("Exchange User-Agent:\n", SunoApi.userAgent); + // console.log("Exchange Cookie:\n", SunoApi.cookie); + const tokenResponse = await axios.post( + exchangeTokenUrl, + {}, + { + headers: { + 'User-Agent': SunoApi.userAgent, + 'Cookie': SunoApi.cookie, + }, + }, + ); + console.log("Token Response:\n", JSON.stringify(tokenResponse.data, null, 2)); + + return tokenResponse.data.jwt; + } + public static async KeepAlive(): Promise { + if (!SunoApi.sid) { + throw new Error("Session ID is not set. Cannot renew token."); + } + // 续订会话令牌的URL + const renewUrl = `${SunoApi.clerkBaseUrl}/v1/client/sessions/${SunoApi.sid}/tokens/api?_clerk_js_version=4.70.0`; + // 续订会话令牌 + const renewResponse = await axios.post( + renewUrl, + {}, + { + headers: { + 'User-Agent': SunoApi.userAgent, + 'Cookie': SunoApi.cookie, + }, + }, + ); + console.log("Renew Response:\n", JSON.stringify(renewResponse.data, null, 2)); + await sleep(1, 2); + const newToken = renewResponse.data.jwt; + // 更新请求头中的Authorization字段,使用新的JWT令牌 + axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`; + } + + public static async generate( + prompt: string, + make_instrumental: boolean = false, + wait_audio: boolean = false, + ): Promise { + + const audios = this.generateSongs(prompt, false, undefined, undefined, make_instrumental, wait_audio); + console.log("Custom Generate Response:\n", JSON.stringify(audios, null, 2)); + return audios; + } + + public static async custom_generate( + prompt: string, + tags: string, + title: string, + make_instrumental: boolean = false, + wait_audio: boolean = false, + ): Promise { + + const audios = await this.generateSongs(prompt, true, tags, title, make_instrumental, wait_audio); + console.log("Custom Generate Response:\n", JSON.stringify(audios, null, 2)); + return audios; + } + + private static async generateSongs( + prompt: string, + isCustom: boolean, + tags?: string, + title?: string, + make_instrumental?: boolean, + wait_audio: boolean = false + ): Promise { + const authToken = await this.getAuthToken(); + const payload: any = { + make_instrumental: make_instrumental == true, + mv: "chirp-v3-0", + prompt: "", + }; + if (isCustom) { + payload.tags = tags; + payload.title = title; + payload.prompt = prompt; + } else { + payload.gpt_description_prompt = prompt; + } + console.log("generateSongs payload:\n", { + prompt: prompt, + isCustom: isCustom, + tags: tags, + title: title, + make_instrumental: make_instrumental, + wait_audio: wait_audio, + payload: payload, + }); + const response = await axios.post( + `${SunoApi.baseUrl}/api/generate/v2/`, + payload, + { + headers: { + 'Authorization': `Bearer ${authToken}`, + 'User-Agent': SunoApi.userAgent, + }, + timeout: 10000, // 10 seconds timeout + }, + ); + console.log("generateSongs Response:\n", JSON.stringify(response.data, null, 2)); + if (response.status !== 200) { + throw new Error("Error response:" + response.statusText); + } + const songIds = response.data.clips.map((audio: any) => audio.id); + //Want to wait for music file generation + if (wait_audio === true) { + const startTime = Date.now(); + let lastResponse: AudioInfo[] = []; + await sleep(2, 4); + while (Date.now() - startTime < 30000) { + const response = await SunoApi.get(songIds); + console.log("Waiting for audio Response:\n", JSON.stringify(response, null, 2)); + const allCompleted = response.every( + audio => audio.status === 'streaming' || audio.status === 'complete' + ); + if (allCompleted) { + return response; + } + lastResponse = response; + await sleep(2, 4); + this.KeepAlive(); + } + return lastResponse; + } else { + this.KeepAlive(); + return response.data.clips.map((audio: any) => ({ + id: audio.id, + title: audio.title, + image_url: audio.image_url, + lyric: audio.metadata.prompt, + audio_url: audio.audio_url, + video_url: audio.video_url, + created_at: audio.created_at, + model_name: audio.model_name, + status: audio.status, + gpt_description_prompt: audio.metadata.gpt_description_prompt, + prompt: audio.metadata.prompt, + type: audio.metadata.type, + tags: audio.metadata.tags, + duration: audio.metadata.duration_formatted, + })); + } + } + /** + * 将音频元数据中的歌词(prompt)处理成易于阅读的格式。 + * @param prompt 原始歌词文本。 + * @returns 处理后的歌词文本。 + */ + private static parseLyrics(prompt: string): string { + // 假设原始歌词是以特定分隔符(例如,换行符)分隔的,我们可以将其转换为更易于阅读的格式。 + // 这里的实现可以根据实际的歌词格式进行调整。 + // 例如,如果歌词是以连续的文本形式存在,可能需要根据特定的标记(如句号、逗号等)来分割。 + // 下面的实现假设歌词已经是以换行符分隔的。 + + // 使用换行符分割歌词,并确保移除空行。 + const lines = prompt.split('\n').filter(line => line.trim() !== ''); + + // 将处理后的歌词行重新组合成一个字符串,每行之间用换行符分隔。 + // 可以在这里添加额外的格式化逻辑,如添加特定的标记或者处理特殊的行。 + const formattedLyrics = lines.join('\n'); + + return formattedLyrics; + } + public static async get(songIds?: string[]): Promise { + const authToken = await this.getAuthToken(); + let url = `${SunoApi.baseUrl}/api/feed/`; + if (songIds) { + url = `${url}?ids=${songIds.join(',')}`; + } + console.log("Get URL:\n", url); + const response = await axios.get(url, { + headers: { + 'Authorization': `Bearer ${authToken}`, + 'User-Agent': SunoApi.userAgent, + }, + timeout: 3000, // 3 seconds timeout + }); + + const audios = response.data; + console.log("Get Response:\n", JSON.stringify(audios, null, 2)); + return audios.map((audio: any) => ({ + id: audio.id, + title: audio.title, + image_url: audio.image_url, + lyric: audio.metadata.prompt ? this.parseLyrics(audio.metadata.prompt) : "", + audio_url: audio.audio_url, + video_url: audio.video_url, + created_at: audio.created_at, + model_name: audio.model_name, + status: audio.status, + gpt_description_prompt: audio.metadata.gpt_description_prompt, + prompt: audio.metadata.prompt, + type: audio.metadata.type, + tags: audio.metadata.tags, + duration: audio.metadata.duration_formatted, + })); + } + + public static async get_limit(): Promise { + const authToken = await this.getAuthToken(); + const response = await axios.get(`${SunoApi.baseUrl}/api/billing/info/`, { + headers: { + 'Authorization': `Bearer ${authToken}`, + 'User-Agent': SunoApi.userAgent, + }, + }); + return response.data.total_credits_left; + } +} + +export default SunoApi; \ No newline at end of file