From 532e7eac8196622043374dfc5238d29d6109b14f Mon Sep 17 00:00:00 2001 From: brokencube Date: Wed, 29 Apr 2026 19:44:25 +0100 Subject: [PATCH] Service decorators - oh the evil I have wraught.... --- .gitignore | 3 +- index.ts | 18 +- package-lock.json | 564 +++++++++++++++++++++++++++++ package.json | 5 + src/base/router.ts | 73 ---- src/core/router.ts | 90 +++++ src/core/service.ts | 168 +++++++++ src/dbTableTypes/migration.ts | 5 + src/interfaces/ServiceInterface.ts | 6 + src/interfaces/TaskInterface.ts | 4 + src/migrations.ts | 22 ++ src/services/CliService.ts | 22 ++ src/services/ConfigService.ts | 7 + src/services/DatabaseService.ts | 65 ++++ src/services/FFMpegService.ts | 44 +++ src/services/TaskService.ts | 32 ++ src/services/WebserverService.ts | 19 + src/services/index.ts | 6 + src/subsonic/router.ts | 2 +- src/tasks/ScanFoldersTask.ts | 43 +++ tsconfig.json | 10 + 21 files changed, 1129 insertions(+), 79 deletions(-) delete mode 100644 src/base/router.ts create mode 100644 src/core/router.ts create mode 100644 src/core/service.ts create mode 100644 src/dbTableTypes/migration.ts create mode 100644 src/interfaces/ServiceInterface.ts create mode 100644 src/interfaces/TaskInterface.ts create mode 100644 src/migrations.ts create mode 100644 src/services/CliService.ts create mode 100644 src/services/ConfigService.ts create mode 100644 src/services/DatabaseService.ts create mode 100644 src/services/FFMpegService.ts create mode 100644 src/services/TaskService.ts create mode 100644 src/services/WebserverService.ts create mode 100644 src/services/index.ts create mode 100644 src/tasks/ScanFoldersTask.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 2d2b47d..ada8e89 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea -node_modules \ No newline at end of file +node_modules/ +database/ \ No newline at end of file diff --git a/index.ts b/index.ts index 9a9da3c..30b8baf 100644 --- a/index.ts +++ b/index.ts @@ -1,14 +1,24 @@ 'use strict'; import '@tsmetadata/polyfill'; -import Koa from 'koa'; import {SubsonicRouter} from "./src/subsonic/router"; +import ScanFoldersTask from "./src/tasks/ScanFoldersTask"; +import {ServiceManager} from "./src/core/service"; +import WebserverService from "./src/services/WebserverService"; +import TaskService from "./src/services/TaskService"; +import * as Services from "./src/services"; try { - const app = new Koa(); - (new SubsonicRouter()).injectInto(app); + const services = ServiceManager.get().mount(Services); - app.listen(8080); + const webserver = services.getService('webserverService'); + webserver.add(new SubsonicRouter()); + + const taskService = services.getService('taskService'); + taskService.addTask(new ScanFoldersTask('/root/jukesquare/public')); + + await services.start(); + webserver.listen(); } catch (e) { console.error(e); } diff --git a/package-lock.json b/package-lock.json index 604edf2..7a7fd3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,15 +10,30 @@ "license": "MIT", "dependencies": { "@koa/router": "^15.4.0", + "@root/walk": "^1.1.0", "@tsmetadata/polyfill": "^1.1.3", + "better-sqlite3": "^12.9.0", + "file-type": "^22.0.1", "koa": "^3.2.0", "xmlbuilder2": "^4.0.3" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/koa": "^3.0.2", + "@types/node": "^25.6.0", "typescript": "^6.0.2" } }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@koa/router": { "version": "15.4.0", "resolved": "https://registry.npmjs.org/@koa/router/-/router-15.4.0.tgz", @@ -90,6 +105,35 @@ "node": ">=20.0" } }, + "node_modules/@root/walk": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@root/walk/-/walk-1.1.0.tgz", + "integrity": "sha512-FfXPAta9u2dBuaXhPRawBcijNC9rmKVApmbi6lIZyg36VR/7L02ytxoY5K/14PJlHqiBUoYII73cTlekdKTUOw==", + "license": "MPL-2.0" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, "node_modules/@tsmetadata/polyfill": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@tsmetadata/polyfill/-/polyfill-1.1.3.tgz", @@ -106,6 +150,16 @@ "@types/node": "*" } }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "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", @@ -305,6 +359,90 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.9.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.9.0.tgz", + "integrity": "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -357,12 +495,36 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "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/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -388,6 +550,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -403,12 +574,54 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "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/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-type": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-22.0.1.tgz", + "integrity": "sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.5", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -418,6 +631,18 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/http-assert": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", @@ -485,12 +710,38 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "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/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -584,12 +835,45 @@ "url": "https://opencollective.com/express" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "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/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -599,6 +883,18 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -611,6 +907,15 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -630,12 +935,155 @@ "url": "https://opencollective.com/express" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "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/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -645,6 +1093,68 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -654,6 +1164,24 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/tsscmp": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", @@ -663,6 +1191,18 @@ "node": ">=0.6.x" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -691,6 +1231,18 @@ "node": ">=14.17" } }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/undici-types": { "version": "7.19.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", @@ -698,6 +1250,12 @@ "dev": true, "license": "MIT" }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -707,6 +1265,12 @@ "node": ">= 0.8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/xmlbuilder2": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz", diff --git a/package.json b/package.json index fd249ab..ddeea50 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,17 @@ }, "dependencies": { "@koa/router": "^15.4.0", + "@root/walk": "^1.1.0", "@tsmetadata/polyfill": "^1.1.3", + "better-sqlite3": "^12.9.0", + "file-type": "^22.0.1", "koa": "^3.2.0", "xmlbuilder2": "^4.0.3" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/koa": "^3.0.2", + "@types/node": "^25.6.0", "typescript": "^6.0.2" } } diff --git a/src/base/router.ts b/src/base/router.ts deleted file mode 100644 index 0e80afe..0000000 --- a/src/base/router.ts +++ /dev/null @@ -1,73 +0,0 @@ -import Router from "@koa/router"; -import Koa from "koa"; - -type Constructor = new (...args: any[]) => T; - -type Route = { - method: string; - path: string; - fn: any; -}; - -const routeSymbol = Symbol("routeSymbol"); - -export function get(path: string) { - return function getDecorator (originalMethod: any, context: ClassMethodDecoratorContext) { - if (context.metadata) { - if (!context.metadata[routeSymbol]) { - context.metadata[routeSymbol] = [] as Route[]; - } - // @ts-ignore - context.metadata[routeSymbol].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[routeSymbol]) { - context.metadata[routeSymbol] = [] as Route[]; - } - // @ts-ignore - context.metadata[routeSymbol].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[routeSymbol]) { - context.metadata[routeSymbol] = []; - } - // @ts-ignore - context.metadata[routeSymbol].unshift({method: 'use', path, fn}); - } - return constructor; - } -} - -export function router(prefix: string) { - return function routerDecorator(constructor: T, context: ClassDecoratorContext) { - return class KoaRouter extends constructor { - public router: Router; - constructor(...args: any[]) { - super(...args); - this.router = new Router({prefix}); - - for (const route of (context.metadata?.[routeSymbol] || []) as Route[]) 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.fn); break; - } - } - - public injectInto(app: Koa) { - app.use(this.router.routes()).use(this.router.allowedMethods()); - } - } - } -} \ No newline at end of file diff --git a/src/core/router.ts b/src/core/router.ts new file mode 100644 index 0000000..e302384 --- /dev/null +++ b/src/core/router.ts @@ -0,0 +1,90 @@ +import Router from "@koa/router"; +import Koa from "koa"; + +type Route = { + method: string; + path: string; + fn: any; +}; + +const routesSymbol = Symbol("symbol.router.routes"); +const routerSymbol = Symbol("symbol.router.router"); + +export class DecoratedRouterCollector { + static routers: {[name: string]: Router} = {}; + + static addRouter(name: string, router: Router) { + this.routers[name] = router; + } + + static bindRouterToApp(instance: object, app: Koa) { + const router = DecoratedRouterCollector.routers[instance.constructor.name]; + if (router instanceof Router) { + app.use(router.routes()).use(router.allowedMethods()); + } else { + throw new Error(`Could not find router metadata on objec type ${instance.constructor.name} - did you remember to add @router decorator to this class?`); + } + } +} + +export function get(path: string) { + return function getDecorator (originalMethod: any, context: ClassMethodDecoratorContext) { + if (context.metadata && !context.metadata[routesSymbol]) { + context.metadata[routesSymbol] = []; + } + + if (context.metadata && Array.isArray(context.metadata[routesSymbol])) { + context.metadata[routesSymbol].push({method: 'get', path, fn: originalMethod}); + } + return originalMethod; + } +} + +export function post(path: string) { + return function getDecorator (originalMethod: any, context: ClassMethodDecoratorContext) { + if (context.metadata && !context.metadata[routesSymbol]) { + context.metadata[routesSymbol] = []; + } + + if (context.metadata && Array.isArray(context.metadata[routesSymbol])) { + context.metadata[routesSymbol].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 && !context.metadata[routesSymbol]) { + context.metadata[routesSymbol] = []; + } + + // If router hasn't already been created, add middleware to list of add to router instance + if (context.metadata && Array.isArray(context.metadata[routesSymbol])) { + context.metadata[routesSymbol].push({method: 'use', path, fn}); + } + + // If router has already been declared, then add middleware to the router directly + if (context.metadata && context.metadata[routerSymbol] instanceof Router) { + context.metadata[routerSymbol].use(fn); + } + + return constructor; + } +} + +export function router(prefix: string) { + return function routerDecorator(constructor: any, context: ClassDecoratorContext) { + const router = new Router({prefix}); + context.metadata[routerSymbol] = router; + + for (const route of (context.metadata[routesSymbol] || []) as Route[]) switch (route.method) { + case "get": router.get(route.path, route.fn); break; + case "post": router.post(route.path, route.fn); break; + case 'use': router.use(route.fn); break; + } + DecoratedRouterCollector.addRouter(constructor.name, router); + + return constructor; + } +} \ No newline at end of file diff --git a/src/core/service.ts b/src/core/service.ts new file mode 100644 index 0000000..181cad3 --- /dev/null +++ b/src/core/service.ts @@ -0,0 +1,168 @@ +import ServiceInterface from "../interfaces/ServiceInterface"; + +type Constructor = new (...args:any[]) => any; + +const serviceNameSymbol = Symbol("symbol.serviceName"); + +export abstract class BaseService implements ServiceInterface { + private startPromise: Promise | null = null; + + async start(): Promise { + if (!this.startPromise) { + await ServiceManager.get().startDependencies( + // @ts-ignore + this.constructor[Symbol.metadata][serviceNameSymbol] + ); + this.startPromise = new Promise(async (resolve) => { + resolve(await this.init()); + }) + } + return this.startPromise; + } + + abstract init(): Promise; + + async stop(): Promise { + await this.startPromise; + await this.destroy(); + this.startPromise = null; + return; + } + + abstract destroy(): Promise; +} + +type ServiceContainer = { + name: string; + service: object | null; + constructor: Constructor | null; + started: boolean; + constructing: boolean; + dependencies: string[]; +} + +export class ServiceManager { + static singleton: ServiceManager; + static get() { + if (!ServiceManager.singleton) ServiceManager.singleton = new ServiceManager(); + return ServiceManager.singleton; + } + + private constructor() {} + + services: Map = new Map(); + + assertService(name: string): ServiceContainer { + if (!this.services.has(name)) { + this.services.set(name, {name, constructor: null, service: null, started: false, constructing: false, dependencies: []}); + } + return this.services.get(name) as ServiceContainer; + } + + addService(name: string, constructor: any) { + const serviceContainer = this.assertService(name); + serviceContainer.constructor = constructor; + } + + addDependency(name: string, dependency: string) { + const service = this.assertService(name); + service.dependencies.push(dependency) + } + + mount(...args: any[]) { + // Ensure all services have been constructed (and dependency injected) + for (const service of this.services.values()) { + if (service.constructor) { + this.injectService(service.name); + } + } + + return this; + } + + async start() { + // Start all constructed services + for (const service of this.services.values()) { + if (service.service && service.service instanceof BaseService) { + await service.service.start(); + } + } + } + + async startDependencies(name: string) { + if (!this.services.has(name)) { + throw new Error(`Unknown service ${name}`); + } + + const service = this.services.get(name) as ServiceContainer; + for (const dependency of service.dependencies) { + const dependentService = this.injectService(dependency); + await dependentService.start(); + } + } + + injectService(name: string) { + if (!this.services.has(name)) { + throw new Error(`Unknown service ${name}`); + } + + const service = this.services.get(name) as ServiceContainer; + if (!service.constructor) { + throw new Error(`service: ${service.name} has no constructor`); + } + if (!service.service) { + if (service.constructing) { + throw new Error(`Dependency cycle detected while constructing service: ${service.name}`); + } + service.constructing = true; + service.service = new service.constructor(); + } + + return service.service as BaseService; + } + + getService(name: string): T { + if (!this.services.has(name)) { + throw new Error(`Unknown service ${name}`); + } + + const service = this.services.get(name) as ServiceContainer; + if (!service.constructor) { + throw new Error(`service: ${service.name} has no constructor`); + } + if (!service.service) { + if (service.constructing) { + throw new Error(`Dependency cycle detected while constructing service: ${service.name}`); + } + service.constructing = true; + service.service = new service.constructor(); + } + + return service.service as T; + } +} + +export function service(propertyName: string) { + return function installService(constructor: any, context: ClassDecoratorContext) { + context.metadata[serviceNameSymbol] = propertyName; + ServiceManager.get().addService(propertyName, constructor); + return constructor; + } +} + +export function inject(name: string) { + return function injectService(value: unknown, context: ClassAccessorDecoratorContext): ClassAccessorDecoratorResult { + if (context.metadata[serviceNameSymbol]) { + ServiceManager.get().addDependency(context.metadata[serviceNameSymbol] as string, name); + } + + return { + get() { + return ServiceManager.get().injectService(name); + }, + set(): never { + throw new Error('Cannot overwrite injected service'); + }, + }; + } +} diff --git a/src/dbTableTypes/migration.ts b/src/dbTableTypes/migration.ts new file mode 100644 index 0000000..9c2b45f --- /dev/null +++ b/src/dbTableTypes/migration.ts @@ -0,0 +1,5 @@ +export default interface DbMigration { + id: number; + name: string; + date: string; +} \ No newline at end of file diff --git a/src/interfaces/ServiceInterface.ts b/src/interfaces/ServiceInterface.ts new file mode 100644 index 0000000..f1f0ed7 --- /dev/null +++ b/src/interfaces/ServiceInterface.ts @@ -0,0 +1,6 @@ +export default interface ServiceInterface { + start(): Promise; + init(): Promise; + stop(): Promise; + destroy(): Promise; +} \ No newline at end of file diff --git a/src/interfaces/TaskInterface.ts b/src/interfaces/TaskInterface.ts new file mode 100644 index 0000000..cbe2850 --- /dev/null +++ b/src/interfaces/TaskInterface.ts @@ -0,0 +1,4 @@ +export default interface TaskInterface { + shouldRun(): boolean; + run(): Promise; +} \ No newline at end of file diff --git a/src/migrations.ts b/src/migrations.ts new file mode 100644 index 0000000..52ee920 --- /dev/null +++ b/src/migrations.ts @@ -0,0 +1,22 @@ +import Database from "better-sqlite3"; + +export type Migration = { + name: string; + migration: (db: Database.Database) => void; +}; + +const baseMigration = (): Migration => { + return { + name: 'baseMigration', + migration: (db) => { + db.prepare('CREATE TABLE "audio" (id INTEGER PRIMARY KEY ASC)').run(); + } + }; +}; + +export function getMigrations() : Migration[] { + return [ + baseMigration() + ]; +}; + diff --git a/src/services/CliService.ts b/src/services/CliService.ts new file mode 100644 index 0000000..2bc8a94 --- /dev/null +++ b/src/services/CliService.ts @@ -0,0 +1,22 @@ +import { spawn } from 'node:child_process'; +import { once } from 'node:events'; + +export async function runCommand(command: string, args: string[]) { + const cmd = spawn(command, args); + const stdout = [] as string[]; + const stderr = [] as string[]; + + cmd.stdout?.on('data', (data: Buffer) => { + stdout.push(data.toString('utf-8')); + }); + + cmd.stderr?.on('data', (data: Buffer) => { + stderr.push(data.toString('utf-8')); + }); + + const [code] = await once(cmd, 'close'); + return { + stdout: stdout.join(''), + stderr: stderr.join(''), + }; +} diff --git a/src/services/ConfigService.ts b/src/services/ConfigService.ts new file mode 100644 index 0000000..cb136bb --- /dev/null +++ b/src/services/ConfigService.ts @@ -0,0 +1,7 @@ +import {getMigrations, Migration} from "../migrations"; +import {service} from "../core/service"; + +@service('configService') +export default class ConfigService { + public migrations: Migration[] = getMigrations(); +} \ No newline at end of file diff --git a/src/services/DatabaseService.ts b/src/services/DatabaseService.ts new file mode 100644 index 0000000..3906687 --- /dev/null +++ b/src/services/DatabaseService.ts @@ -0,0 +1,65 @@ +import Database from 'better-sqlite3'; +import DbMigration from "../dbTableTypes/migration"; +import {Migration} from "../migrations"; +import ConfigService from "./ConfigService"; +import {BaseService, inject, service} from "../core/service"; + +const MIGRATION_TABLE_NAME = "migration"; + +@service('databaseService') +export default class DatabaseService extends BaseService { + protected db: Database.Database; + + @inject('configService') protected accessor config!: ConfigService; + + constructor() { + super(); + this.db = new Database('database/core.db'); + this.db.pragma('journal_mode = WAL'); + } + + async init() { + const migrations = this.config.migrations; + + this.assertMigrationTable(); + + // Turn array of migrations into dictionary + const migrationDict = {} as {[key: string]: Migration}; + for (const migration of migrations) { + migrationDict[migration.name] = migration; + } + + // Remove any already run migrations from the dict + const existingMigrations = this.getMigrations(); + for (const existing of existingMigrations) { + if (migrationDict[existing.name]) { + delete migrationDict[existing.name]; + } + } + + // Run any remaining migrations + for (const migration of Object.values(migrationDict)) { + migration.migration(this.db); + this.saveExecutedMigration(migration); + } + } + + async destroy() { + + } + + getMigrations(): DbMigration[] { + return this.db.prepare(`SELECT * FROM ${MIGRATION_TABLE_NAME};`).all() as DbMigration[]; + } + + saveExecutedMigration(migration: Migration) { + this.db.prepare(`INSERT into ${MIGRATION_TABLE_NAME}(name) values (:name)`).run({name: migration.name}); + } + + assertMigrationTable() { + const migrationTable = this.db.prepare(`SELECT name FROM sqlite_master WHERE name='${MIGRATION_TABLE_NAME}'`).get(); + if (!migrationTable) { + this.db.prepare(`CREATE TABLE ${MIGRATION_TABLE_NAME} (id INTEGER PRIMARY KEY ASC, name VARCHAR, date TIMESTAMP DEFAULT CURRENT_TIMESTAMP)`).run(); + } + } +} \ No newline at end of file diff --git a/src/services/FFMpegService.ts b/src/services/FFMpegService.ts new file mode 100644 index 0000000..234123b --- /dev/null +++ b/src/services/FFMpegService.ts @@ -0,0 +1,44 @@ +import {runCommand} from "./CliService"; +import {service} from "../core/service"; + +@service('ffmpegService') +export default class FFMpegService { + static async checkFile(filename: string) { + const file = await runCommand('ffprobe', [ + '-v', + 'quiet', + '-print_format', + 'json', + '-show_format', + filename + ]); + + if (file.stderr !== '') { + throw new Error('FFMpeg returned error: ' + file.stderr); + } + + const json = JSON.parse(file.stdout); + + if (!json || !json.format) { + throw new Error('FFMpeg did not return format block: ' + file.stdout); + } + + return { + type: json.format.format_name, + duration: parseFloat(json.format.duration), + size: parseFloat(json.format.size), + tags: { + title: json.format.tags?.title || json.format.tags?.TITLE || null, + album: json.format.tags?.album || json.format.tags?.ALBUM || null, + artist: json.format.tags?.artist || json.format.tags?.ARTIST || null, + comment: json.format.tags?.comment || json.format.tags?.COMMENT || null, + track: json.format.tags?.track || json.format.tags?.TRACK || null, + genre: json.format.tags?.genre || json.format.tags?.GENRE || null, + publisher: json.format.tags?.publisher || json.format.tags?.PUBLISHER || null, + album_artist: json.format.tags?.album_artist || json.format.tags?.ALBUM_ARTIST || null, + composer: json.format.tags?.composer || json.format.tags?.COMPOSER || null, + year: json.format.tags?.year || json.format.tags?.YEAR || null, + }, + }; + } +} \ No newline at end of file diff --git a/src/services/TaskService.ts b/src/services/TaskService.ts new file mode 100644 index 0000000..6e18c5f --- /dev/null +++ b/src/services/TaskService.ts @@ -0,0 +1,32 @@ +import TaskInterface from "../interfaces/TaskInterface"; +import {BaseService, service} from "../core/service"; + +const FIVE_MINUTES: number = 5 * 60 * 1000; + +@service('taskService') +export default class TaskService extends BaseService { + protected tasks: TaskInterface[] = []; + private timeout: NodeJS.Timeout | null = null; + + public addTask(task: TaskInterface) : void { + this.tasks.push(task); + } + + async init() { + this.timeout = setTimeout(async () => { + await this.run(); + }, 0); + } + + async run() { + this.tasks.forEach(task => { + if (task.shouldRun()) task.run(); + }); + this.timeout = setTimeout(() => this.run, FIVE_MINUTES); + } + + async destroy() { + if (this.timeout) clearTimeout(this.timeout); + this.timeout = null; + } +} \ No newline at end of file diff --git a/src/services/WebserverService.ts b/src/services/WebserverService.ts new file mode 100644 index 0000000..39a7dba --- /dev/null +++ b/src/services/WebserverService.ts @@ -0,0 +1,19 @@ +import Koa from "koa"; +import {DecoratedRouterCollector} from "../core/router"; +import {BaseService, service} from "../core/service"; + +@service('webserverService') +export default class WebserverService extends BaseService { + private app: Koa = new Koa(); + + add(router: any) { + DecoratedRouterCollector.bindRouterToApp(router, this.app); + } + + listen(port: number = 8080) { + this.app.listen(port); + } + + async init() {} + async destroy() {} +} \ No newline at end of file diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000..54677f4 --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,6 @@ +export * from './CliService'; +export * from './TaskService'; +export * from './ConfigService'; +export * from './FFMpegService'; +export * from './DatabaseService'; +export * from './WebserverService'; \ No newline at end of file diff --git a/src/subsonic/router.ts b/src/subsonic/router.ts index 836079e..ad67c68 100644 --- a/src/subsonic/router.ts +++ b/src/subsonic/router.ts @@ -1,4 +1,4 @@ -import {get, middleware, post, router} from "../base/router"; +import {get, middleware, post, router} from "../core/router"; import Koa from "koa"; import {extractSubsonicApiContext, subsonicErrorHandler} from "./middleware"; import {Renderer} from "./renderer"; diff --git a/src/tasks/ScanFoldersTask.ts b/src/tasks/ScanFoldersTask.ts new file mode 100644 index 0000000..784fe01 --- /dev/null +++ b/src/tasks/ScanFoldersTask.ts @@ -0,0 +1,43 @@ +// @ts-ignore +import { walk } from '@root/walk'; +import {Dirent} from "node:fs"; +import {fileTypeFromFile} from "file-type"; +import FFMpegService from "../services/FFMpegService"; +import TaskInterface from "../interfaces/TaskInterface"; + +export default class ScanFoldersTask implements TaskInterface { + private hasRun: boolean = false; + + constructor(protected mount: string) { + } + + shouldRun(): boolean { + return !this.hasRun; + } + + async run(): Promise { + this.hasRun = true; + return this.scanFolder(); + } + + async scanFolder() { + return walk(this.mount, async (err: any, pathname: string, dirent: Dirent) => { + if (err) return false; + if (dirent.isDirectory()) await this.processFolder(pathname, dirent); + if (dirent.isFile()) await this.processFile(pathname, dirent); + return true; + }); + } + + async processFolder(pathname: string, dirent: Dirent) { + console.log('folder', pathname); + } + + async processFile(pathname: string, dirent: Dirent) { + console.log('file', pathname); + const type = await fileTypeFromFile(pathname); + if (type === undefined || type.mime.startsWith('audio')) { + console.log(await FFMpegService.checkFile(pathname)); + } + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a7f3786 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "esnext", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + } +}