From 8e5a8d073fb2ead13b33fb671798de7d00772607 Mon Sep 17 00:00:00 2001 From: brokencube Date: Wed, 8 Apr 2026 19:40:39 +0100 Subject: [PATCH] Initial commit with decorator router, and some basic subsonic api code --- .gitignore | 2 + index.ts | 14 + package-lock.json | 726 ++++++++++++++++++ package.json | 26 + src/base/router.ts | 80 ++ src/enum/SubsonicErrorCode.ts | 13 + src/enum/SubsonicResponseFormat.ts | 5 + src/enum/index.ts | 2 + src/subsonic/context.ts | 133 ++++ src/subsonic/error.ts | 16 + .../middleware/extractSubsonicApiContext.ts | 7 + src/subsonic/middleware/index.ts | 2 + .../middleware/subsonicErrorHandler.ts | 35 + src/subsonic/renderBackends/index.ts | 3 + src/subsonic/renderBackends/json-responder.ts | 20 + .../renderBackends/jsonp-responder.ts | 13 + src/subsonic/renderBackends/xml-responder.ts | 29 + src/subsonic/renderer.ts | 30 + src/subsonic/router.ts | 22 + 19 files changed, 1178 insertions(+) create mode 100644 .gitignore create mode 100644 index.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/base/router.ts create mode 100644 src/enum/SubsonicErrorCode.ts create mode 100644 src/enum/SubsonicResponseFormat.ts create mode 100644 src/enum/index.ts create mode 100644 src/subsonic/context.ts create mode 100644 src/subsonic/error.ts create mode 100644 src/subsonic/middleware/extractSubsonicApiContext.ts create mode 100644 src/subsonic/middleware/index.ts create mode 100644 src/subsonic/middleware/subsonicErrorHandler.ts create mode 100644 src/subsonic/renderBackends/index.ts create mode 100644 src/subsonic/renderBackends/json-responder.ts create mode 100644 src/subsonic/renderBackends/jsonp-responder.ts create mode 100644 src/subsonic/renderBackends/xml-responder.ts create mode 100644 src/subsonic/renderer.ts create mode 100644 src/subsonic/router.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d2b47d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +node_modules \ No newline at end of file diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..9a9da3c --- /dev/null +++ b/index.ts @@ -0,0 +1,14 @@ +'use strict'; + +import '@tsmetadata/polyfill'; +import Koa from 'koa'; +import {SubsonicRouter} from "./src/subsonic/router"; + +try { + const app = new Koa(); + (new SubsonicRouter()).injectInto(app); + + app.listen(8080); +} catch (e) { + console.error(e); +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a293052 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,726 @@ +{ + "name": "jukesquare-backend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "jukesquare-backend", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "@koa/router": "^15.4.0", + "@tsmetadata/polyfill": "^1.1.3", + "koa": "^3.2.0", + "xmlbuilder2": "^4.0.3" + }, + "devDependencies": { + "@types/koa": "^3.0.2", + "typescript": "^6.0.2" + } + }, + "node_modules/@koa/router": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-15.4.0.tgz", + "integrity": "sha512-vKYlXtoCfcAN8z4dHiveYX55rTYOgHEYJNumK1WM9ZAwaArhreGVkyC1LTMGfUQUJyIO/SbwRFBOHeOCY8/MaQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "koa-compose": "^4.1.0", + "path-to-regexp": "^8.3.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "koa": "^2.0.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "koa": { + "optional": false + } + } + }, + "node_modules/@oozcitak/dom": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-2.0.2.tgz", + "integrity": "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==", + "license": "MIT", + "dependencies": { + "@oozcitak/infra": "^2.0.2", + "@oozcitak/url": "^3.0.0", + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/infra": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-2.0.2.tgz", + "integrity": "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA==", + "license": "MIT", + "dependencies": { + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-3.0.0.tgz", + "integrity": "sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ==", + "license": "MIT", + "dependencies": { + "@oozcitak/infra": "^2.0.2", + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/util": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-10.0.0.tgz", + "integrity": "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==", + "license": "MIT", + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@tsmetadata/polyfill": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@tsmetadata/polyfill/-/polyfill-1.1.3.tgz", + "integrity": "sha512-uSRn4aPO4F3wlSG/cPCAvclF9Sxf01OizWfWbSoSYsSHAK8LnYdua9iJAm7v2ePUrGWMP4ZCn9QjniZEKZHyFg==", + "license": "MIT" + }, + "node_modules/@types/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/content-disposition": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz", + "integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cookies": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.2.tgz", + "integrity": "sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-assert": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.6.tgz", + "integrity": "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keygrip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/koa": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-3.0.2.tgz", + "integrity": "sha512-7TRzVOBcH/q8CfPh9AmHBQ8TZtimT4Sn+rw8//hXveI6+F41z93W8a+0B0O8L7apKQv+vKBIEZSECiL0Oo1JFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "^2", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "node_modules/@types/koa-compose": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.9.tgz", + "integrity": "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/koa": "*" + } + }, + "node_modules/@types/node": { + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", + "license": "MIT" + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", + "license": "MIT", + "dependencies": { + "deep-equal": "~1.0.1", + "http-errors": "~1.8.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-assert/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "license": "MIT", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/koa/-/koa-3.2.0.tgz", + "integrity": "sha512-TrM4/tnNY7uJ1aW55sIIa+dqBvc4V14WRIAlGcWat9wV5pRS9Wr5Zk2ZTjQP1jtfIHDoHiSbPuV08P0fUZo2pg==", + "license": "MIT", + "dependencies": { + "accepts": "^1.3.8", + "content-disposition": "~1.0.1", + "content-type": "^1.0.5", + "cookies": "~0.9.1", + "delegates": "^1.0.0", + "destroy": "^1.2.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.5.0", + "http-errors": "^2.0.0", + "koa-compose": "^4.1.0", + "mime-types": "^3.0.1", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "license": "MIT" + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/xmlbuilder2": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz", + "integrity": "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==", + "license": "MIT", + "dependencies": { + "@oozcitak/dom": "^2.0.2", + "@oozcitak/infra": "^2.0.2", + "@oozcitak/util": "^10.0.0", + "js-yaml": "^4.1.1" + }, + "engines": { + "node": ">=20.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fd249ab --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "jukesquare-backend", + "version": "0.0.0", + "description": "", + "repository": { + "type": "git", + "url": "https://git.brokencube.co.uk/brokencube/jukesquare-backend.git" + }, + "license": "MIT", + "author": "", + "type": "module", + "main": "index.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@koa/router": "^15.4.0", + "@tsmetadata/polyfill": "^1.1.3", + "koa": "^3.2.0", + "xmlbuilder2": "^4.0.3" + }, + "devDependencies": { + "@types/koa": "^3.0.2", + "typescript": "^6.0.2" + } +} diff --git a/src/base/router.ts b/src/base/router.ts new file mode 100644 index 0000000..659f5bc --- /dev/null +++ b/src/base/router.ts @@ -0,0 +1,80 @@ +import Router from "@koa/router"; +import Koa from "koa"; + +type Constructor = new (...args: any[]) => T; + +type Route = { + method: string; + path: string; + fn: any; +}; + +export class BaseRouter { + public router!: Router; + + processRoutes(routes: Route[]) { + for (const route of routes) switch (route.method) { + case "get": this.router.get(route.path, route.fn); break; + case "post": this.router.post(route.path, route.fn); break; + case 'use': this.router.use(route.path, route.fn); break; + } + } + + injectInto(app: Koa) { + app.use(this.router.routes()).use(this.router.allowedMethods()); + } +} + +export function get(path: string) { + return function getDecorator (originalMethod: any, context: ClassMethodDecoratorContext) { + if (context.metadata) { + if (!context.metadata.routes) { + context.metadata.routes = []; + } + // @ts-ignore + context.metadata.routes.push({method: 'get', path, fn: originalMethod}); + } + return originalMethod; + } +} + +export function post(path: string) { + return function getDecorator (originalMethod: any, context: ClassMethodDecoratorContext) { + if (context.metadata) { + if (!context.metadata.routes) { + context.metadata.routes = []; + } + // @ts-ignore + context.metadata.routes.push({method: 'post', path, fn: originalMethod}); + } + return originalMethod; + } +} + +export function middleware(fn: any, path: string = '') { + return function middlewareDecorator (constructor: any, context: ClassDecoratorContext) { + if (context.metadata) { + if (!context.metadata.routes) { + context.metadata.routes = []; + } + // @ts-ignore + context.metadata.routes.unshift({method: 'use', path, fn}); + } + return constructor; + } +} + +export function router(prefix: string) { + return function routerDecorator(constructor: T, context: ClassDecoratorContext) { + return class extends constructor { + constructor(...args: any[]) { + super(...args); + if (this instanceof BaseRouter) { + this.router = new Router({prefix}); + // @ts-ignore + this.processRoutes(context.metadata?.routes || [] satisfies Route[]); + } + } + } + } +} \ No newline at end of file diff --git a/src/enum/SubsonicErrorCode.ts b/src/enum/SubsonicErrorCode.ts new file mode 100644 index 0000000..985ce07 --- /dev/null +++ b/src/enum/SubsonicErrorCode.ts @@ -0,0 +1,13 @@ +export enum SubsonicErrorCode { + Generic = 0, + RequiredParam = 10, + ClientUpgradeRequired = 20, + ServerUpgradeRequired = 30, + InvalidAuth = 40, + TokenAuthNotSupported = 41, + SpecifiedAuthNotSupported = 42, + ConflictingAuthSchemes = 43, + InvalidApikey = 44, + NotAuthorized = 50, + NotFound = 70, +} \ No newline at end of file diff --git a/src/enum/SubsonicResponseFormat.ts b/src/enum/SubsonicResponseFormat.ts new file mode 100644 index 0000000..71ba277 --- /dev/null +++ b/src/enum/SubsonicResponseFormat.ts @@ -0,0 +1,5 @@ +export enum SubsonicResponseFormat { + XML, + JSON, + JSONP +} \ No newline at end of file diff --git a/src/enum/index.ts b/src/enum/index.ts new file mode 100644 index 0000000..361e481 --- /dev/null +++ b/src/enum/index.ts @@ -0,0 +1,2 @@ +export * from './SubsonicErrorCode'; +export * from './SubsonicResponseFormat'; \ No newline at end of file diff --git a/src/subsonic/context.ts b/src/subsonic/context.ts new file mode 100644 index 0000000..e725179 --- /dev/null +++ b/src/subsonic/context.ts @@ -0,0 +1,133 @@ +import * as crypto from "node:crypto"; +import {SubsonicResponseFormat, SubsonicErrorCode} from "../enum"; +import Koa from "koa"; +import {SubsonicError} from "./error"; + +export type SubsonicContextPrimitive = { + format: SubsonicResponseFormat; + apikey: string | null; + version: string | null; + client: string | null; + callbackName: string | null; +}; + +export const STATIC_SUBSONIC_PASSWORD = 'apikey'; + +export class SubsonicContext { + constructor( + public readonly format: SubsonicResponseFormat, + public readonly apikey: string | null = null, + public readonly version: string | null = null, + public readonly client: string | null = null, + public readonly callbackName: string | null = null, + ) { + if (this.version === null) this.version = '1.1.0'; + if (!this.callbackName) this.callbackName = 'parseResponse'; + } + + static fromQueryParams(req: Koa.Request) { + const queryParams = SubsonicContext.emptyPrimitive(); + try { + if (req.query['f'] === 'json') { + queryParams.format = SubsonicResponseFormat.JSON; + } + + if (req.query['f'] === 'jsonp') { + queryParams.format = SubsonicResponseFormat.JSONP; + if (req.query['callback'] && !Array.isArray(req.query['callback'])) { + queryParams.callbackName = req.query['callback']; + } + } + + if (!req.query['v'] || Array.isArray(req.query['v'])) { + throw new Error('No api version provided'); + } + queryParams.version = req.query['v']; + + if (req.query['apikey']) { + if (req.query['u'] || req.query['t'] || req.query['s'] || req.query['p']) { + throw new SubsonicError(SubsonicErrorCode.ConflictingAuthSchemes, 'Cannot provide params "p", "t", "s", "u" when specifying "apikey"'); + } + if (Array.isArray(req.query['apikey'])) { + throw new SubsonicError(SubsonicErrorCode.ConflictingAuthSchemes, 'Multiple "apikey" params provided'); + } + queryParams.apikey = req.query['apikey']; + } else { + if (req.query['p']) { + throw new SubsonicError(SubsonicErrorCode.SpecifiedAuthNotSupported, 'Subsonic API password authentication not supported in JukeSquare'); + } + + if (!req.query['t'] || Array.isArray(req.query['t'])) { + throw new SubsonicError(SubsonicErrorCode.InvalidAuth, '(Legacy Auth) No token provided'); + } + + if (!req.query['s'] || Array.isArray(req.query['s'])) { + throw new SubsonicError(SubsonicErrorCode.InvalidAuth, '(Legacy Auth) No token salt provided'); + } + + /* For backwards compatability with older Subsonic clients, we support "user and password" auth using + * a valid apikey as the username and static string "apikey" as the password. + * We force this specific password to allow dropping out very early if we suspect a real username/password pair have been provided. + * Effectively this is a way of "opting-in" to legacy auth support. + */ + const expectedToken = SubsonicContext.hashToken(STATIC_SUBSONIC_PASSWORD, req.query['s']); + if (req.query['t'] !== expectedToken) { + throw new SubsonicError(SubsonicErrorCode.InvalidAuth, '(Legacy Auth) Token password was not provided as "apikey" - Subsonic Api support in JukeSquare requires apikey to be provided as username and "apikey" provided as password'); + } + + if (!req.query['u'] || Array.isArray(req.query['u'])) { + throw new SubsonicError(SubsonicErrorCode.InvalidAuth, '(Legacy Auth) No user provided - user\'s apikey must be provided in username field'); + } + queryParams.apikey = req.query['u']; + } + + if (!req.query['c'] || Array.isArray(req.query['c'])) { + throw new SubsonicError(SubsonicErrorCode.InvalidAuth, 'No client name provided'); + } + queryParams.client = req.query['c']; + + return SubsonicContext.fromPrimitive(queryParams); + } catch (e) { + /* Inject whatever context we have gathered into the error before bubbling up */ + if (e instanceof SubsonicError) { + e.setContext(SubsonicContext.fromPrimitive(queryParams)); + } + throw e; + } + + } + + static empty(): SubsonicContext { + return new SubsonicContext( + SubsonicResponseFormat.XML, + null, + null, + null, + null + ); + } + + static fromPrimitive(primitive: SubsonicContextPrimitive): SubsonicContext { + return new SubsonicContext( + primitive.format, + primitive.apikey, + primitive.version, + primitive.client, + primitive.callbackName, + ); + } + + static emptyPrimitive(): SubsonicContextPrimitive { + return { + format: SubsonicResponseFormat.XML, + apikey: null, + version: null, + client: null, + callbackName: null, + } satisfies SubsonicContextPrimitive; + } + + static hashToken(token: string, salt: string): string { + return crypto.createHash('md5').update(`${token}${salt}`).digest('hex'); + } +} \ No newline at end of file diff --git a/src/subsonic/error.ts b/src/subsonic/error.ts new file mode 100644 index 0000000..0d1c38f --- /dev/null +++ b/src/subsonic/error.ts @@ -0,0 +1,16 @@ +import {SubsonicErrorCode} from "../enum"; +import {SubsonicContext} from "./context"; + +export class SubsonicError extends Error { + constructor( + public code: SubsonicErrorCode, + public message: string = '', + public context: SubsonicContext | null = null, + ) { + super(message); + } + + public setContext(context: SubsonicContext): void { + this.context = context; + } +} \ No newline at end of file diff --git a/src/subsonic/middleware/extractSubsonicApiContext.ts b/src/subsonic/middleware/extractSubsonicApiContext.ts new file mode 100644 index 0000000..bb2940d --- /dev/null +++ b/src/subsonic/middleware/extractSubsonicApiContext.ts @@ -0,0 +1,7 @@ +import Koa from "koa"; +import {SubsonicContext} from "../context"; + +export async function extractSubsonicApiContext(ctx: Koa.Context, next: () => Promise) { + ctx.subsonicContext = SubsonicContext.fromQueryParams(ctx.request); + await next(); +} \ No newline at end of file diff --git a/src/subsonic/middleware/index.ts b/src/subsonic/middleware/index.ts new file mode 100644 index 0000000..7fbe838 --- /dev/null +++ b/src/subsonic/middleware/index.ts @@ -0,0 +1,2 @@ +export * from './subsonicErrorHandler'; +export * from './extractSubsonicApiContext'; \ No newline at end of file diff --git a/src/subsonic/middleware/subsonicErrorHandler.ts b/src/subsonic/middleware/subsonicErrorHandler.ts new file mode 100644 index 0000000..33b091a --- /dev/null +++ b/src/subsonic/middleware/subsonicErrorHandler.ts @@ -0,0 +1,35 @@ +import {SubsonicError} from "../error"; +import Koa from "koa"; +import {SubsonicErrorCode} from "../../enum/SubsonicErrorCode"; +import {SubsonicContext} from "../context"; +import {Renderer} from "../renderer"; + +export async function subsonicErrorHandler(ctx: Koa.Context, next: () => Promise) { + try { + await next(); + } catch (e) { + if (e instanceof SubsonicError) { + ctx.body = Renderer.renderError(e); + return; + } + const context = ctx.store?.subsonicContext ?? SubsonicContext.empty(); + + if (e instanceof Error) { + const genericError = new SubsonicError( + SubsonicErrorCode.Generic, + `Generic Error: ${e.message}`, + context + ); + + ctx.body = Renderer.renderError(genericError); + return; + } + + const unknownError = new SubsonicError( + SubsonicErrorCode.Generic, + `Unknown Error - JSON representation of error object returned: ${JSON.stringify(e)}`, + context + ); + ctx.body = Renderer.renderError(unknownError); + } +} \ No newline at end of file diff --git a/src/subsonic/renderBackends/index.ts b/src/subsonic/renderBackends/index.ts new file mode 100644 index 0000000..4a8589b --- /dev/null +++ b/src/subsonic/renderBackends/index.ts @@ -0,0 +1,3 @@ +export * from './xml-responder'; +export * from './json-responder'; +export * from './jsonp-responder'; \ No newline at end of file diff --git a/src/subsonic/renderBackends/json-responder.ts b/src/subsonic/renderBackends/json-responder.ts new file mode 100644 index 0000000..ea51a13 --- /dev/null +++ b/src/subsonic/renderBackends/json-responder.ts @@ -0,0 +1,20 @@ +import {SubsonicErrorCode} from "../../enum/SubsonicErrorCode"; + +export class JsonResponder { + static renderError(code: SubsonicErrorCode, message: string): string { + return JSON.stringify({ + "subsonic-response": { + status: "failed", + version: "1.16.1", + error:{ + code, + message + } + } + }); + } + + static createResponse(element: string | null): string { + return ''; + } +} \ No newline at end of file diff --git a/src/subsonic/renderBackends/jsonp-responder.ts b/src/subsonic/renderBackends/jsonp-responder.ts new file mode 100644 index 0000000..76fbfaf --- /dev/null +++ b/src/subsonic/renderBackends/jsonp-responder.ts @@ -0,0 +1,13 @@ +import {SubsonicErrorCode} from "../../enum/SubsonicErrorCode"; +import {JsonResponder} from "./json-responder"; + +export class JsonPResponder { + static renderError(code: SubsonicErrorCode, message: string, callback: string): string { + const json = JsonResponder.renderError(code, message); + return `${callback}(${json});`; + } + + static createResponse(element: string | null): string { + return ''; + } +} \ No newline at end of file diff --git a/src/subsonic/renderBackends/xml-responder.ts b/src/subsonic/renderBackends/xml-responder.ts new file mode 100644 index 0000000..c7c42bc --- /dev/null +++ b/src/subsonic/renderBackends/xml-responder.ts @@ -0,0 +1,29 @@ +import {create} from 'xmlbuilder2'; +import {SubsonicErrorCode} from "../../enum/SubsonicErrorCode"; + +export class XmlResponder { + private static buildRoot(ok: boolean = true) { + return create({version: '1.0', encoding: 'utf-8'}) + .ele('subsonic-response', { + xmlns: 'http://subsonic.org/restapi', + status: ok ? 'ok' : 'failed', + version: "1.16.1", + }) + } + + static renderError(code: SubsonicErrorCode, message: string) { + return XmlResponder.buildRoot( false) + .ele('error', { + code, + message, + }) + .end({prettyPrint: true}); + } + + static createResponse(element: object | null): any { + const root = XmlResponder.buildRoot(); + if (element) {} + + return root.end({prettyPrint: true}); + } +} \ No newline at end of file diff --git a/src/subsonic/renderer.ts b/src/subsonic/renderer.ts new file mode 100644 index 0000000..1983fee --- /dev/null +++ b/src/subsonic/renderer.ts @@ -0,0 +1,30 @@ +import {SubsonicResponseFormat} from "../enum"; +import {SubsonicError} from "./error"; +import {SubsonicContext} from "./context"; +import {JsonPResponder, JsonResponder, XmlResponder} from "./renderBackends"; + +export class Renderer extends Error { + static render(message: object | null, context: SubsonicContext | null = null) { + switch (context?.format) { + case SubsonicResponseFormat.XML: + default: + return XmlResponder.createResponse(message); + case SubsonicResponseFormat.JSON: + return ''; + case SubsonicResponseFormat.JSONP: + return ''; + } + } + + static renderError(e: SubsonicError) { + switch (e.context?.format) { + case SubsonicResponseFormat.XML: + default: + return XmlResponder.renderError(e.code, e.message); + case SubsonicResponseFormat.JSON: + return JsonResponder.renderError(e.code, e.message); + case SubsonicResponseFormat.JSONP: + return JsonPResponder.renderError(e.code, e.message, e.context?.callbackName || 'parseResponse'); + } + } +} \ No newline at end of file diff --git a/src/subsonic/router.ts b/src/subsonic/router.ts new file mode 100644 index 0000000..39f7851 --- /dev/null +++ b/src/subsonic/router.ts @@ -0,0 +1,22 @@ +import {BaseRouter, get, middleware, post, router} from "../base/router"; +import Koa from "koa"; +import {extractSubsonicApiContext, subsonicErrorHandler} from "./middleware"; +import {Renderer} from "./renderer"; + +@router('/rest') +@middleware(subsonicErrorHandler) +@middleware(extractSubsonicApiContext) +export class SubsonicRouter extends BaseRouter { + @get('/ping') + ping(ctx: Koa.Context) { + console.log('ping'); + ctx.body = Renderer.render(null, ctx.subsonicContext) + } + + @get('/ping2') + @post('/ping2') + ping2(ctx: Koa.Context) { + console.log('ping'); + ctx.body = Renderer.render(null, ctx.subsonicContext) + } +} \ No newline at end of file